/**
 * Copyright (c) 2013 gootara.org <http://gootara.org>
 *
 * The MIT License (MIT)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.gootara.ios.image.util.ui;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
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.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JSeparator;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.SpringLayout;
import javax.swing.SwingConstants;
import javax.swing.SwingWorker;
import javax.swing.TransferHandler;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.filechooser.FileNameExtensionFilter;

import org.gootara.ios.image.util.IOSAssetCatalogs;
import org.gootara.ios.image.util.IOSAssetCatalogs.JSON_VALUE;
import org.gootara.ios.image.util.IOSImageSet;
import org.gootara.ios.image.util.IOSImageUtil;

/**
 * The main window frame of IOSImageUtil.
 *
 * @author gootara.org
 */
public class SplitterFrame extends JFrame {
	static final int[] COMPRESSION_QUALITIES = { 100, 10, 30, 60, 80, 100 };
	private MainFrame owner;
	private JPanel settings, card;
	private JTabbedPane deviceSpecificTabs;
	private JToggleButton universal, deviceSpecific;
	private JCheckBox overwriteAlways, asImageSets, cleanBeforeGenerate;
	private JComboBox widthType, heightType, scalingType, renderAs, outputAs;
	private JLabel widthLabel, heightLabel;
	private JTextField subdir, bgcolor;
	private JButton splitButton;
	private File previousTargetDirectory, previousOutputDirectory;
	private SplitterSizePanel sizeDefault, sizeUniversal, sizeiPhone, sizeiPad, sizeAppleWatch, sizeMac;
	private JProgressBar progress;
	private ExecutorService pool;
	private HashMap<File, ArrayList<IOSImageSet>> contentCache;

	/**
	 * Constructor
	 *
	 * @param owner
	 * @param title
	 */
	public SplitterFrame(MainFrame owner, String title) {
		super(title);
		this.owner = owner;
		//super(owner, title, Dialog.ModalityType.MODELESS);
		initFrame();
	}

//	protected void frameInit() {
	private void initFrame() {
		ArrayList<Image> icons = new ArrayList<Image>();
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		icons.add(toolkit.getImage(this.getClass().getResource("img/icon16.png")));
		icons.add(toolkit.getImage(this.getClass().getResource("img/icon24.png")));
		icons.add(toolkit.getImage(this.getClass().getResource("img/icon32.png")));
		icons.add(toolkit.getImage(this.getClass().getResource("img/icon48.png")));
		icons.add(toolkit.getImage(this.getClass().getResource("img/icon64.png")));
		icons.add(toolkit.getImage(this.getClass().getResource("img/icon96.png")));
		icons.add(toolkit.getImage(this.getClass().getResource("img/icon128.png")));
		icons.add(toolkit.getImage(this.getClass().getResource("img/icon256.png")));
		this.setIconImages(icons);
		contentCache = new HashMap<File, ArrayList<IOSImageSet>>();
		final String placeHolder = owner.getResource("splitter.label", "Same as origin.");
		boolean isMacLAF = UIManager.getLookAndFeel().getName().toLowerCase().indexOf("mac") >= 0;
		this.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
		this.setLayout(new BorderLayout());
		this.setSize(320, 544);
		this.setResizable(false);
		this.setBackground(Color.WHITE);
		Font fontLarge = new Font(owner.getResource("font.default.name", Font.SANS_SERIF), Font.PLAIN, 14);
		Font fontNormal = new Font(owner.getResource("font.default.name", Font.SANS_SERIF), Font.PLAIN, 12);
		Font fontSmall = new Font(owner.getResource("font.default.name", Font.SANS_SERIF), Font.PLAIN, 11);
		previousOutputDirectory = null;

		ItemListener propertyChangedItemListener = (new ItemListener() {
			@Override public void itemStateChanged(ItemEvent e) {
				owner.setStorePropertiesRequested(true);
				applyComponentStatus();
			}
		});

		settings = new JPanel();
		settings.setLayout(new BorderLayout());
		settings.setBackground(Color.WHITE);

		JPanel settingsNorth = new JPanel();
		settingsNorth.setBackground(Color.WHITE);
		settingsNorth.add(this.asImageSets = new JCheckBox(owner.getResource("splitter.as.imagesets", "Generate images as ImageSets"), false));
		settingsNorth.add(new JSeparator(JSeparator.VERTICAL));
		settingsNorth.add(this.universal = new JToggleButton(owner.getResource("splitter.toggle.universal", "   Universal   "), true));
		settingsNorth.add(this.deviceSpecific = new JToggleButton(owner.getResource("splitter.toggle.device.specific", "Device Specific"), false));
		this.asImageSets.setToolTipText(owner.getResource("splitter.as.imagesets.tooltip", "ImageSets directory and Contents.json will be generated automatically. It could be to copy & paste to the Xcode project."));
		this.asImageSets.setBackground(Color.WHITE);
		this.asImageSets.addItemListener(propertyChangedItemListener);
		this.asImageSets.addItemListener(new ItemListener() {
			@Override public void itemStateChanged(ItemEvent e) {
				String imagesXcassets = owner.getResource("string.dir.assets", "Images.xcassets");
				if (asImageSets.isSelected()) {
					if (subdir.getText().trim().isEmpty() || subdir.getText().trim().equals(placeHolder)) {
						setOutputDirectory(imagesXcassets);
					}
				} else {
					if (subdir.getText().trim().equals(imagesXcassets)) {
						setOutputDirectory("");
					}
				}
			}
		});

		this.universal.setBackground(Color.WHITE);
		this.universal.addItemListener(propertyChangedItemListener);
		this.deviceSpecific.setBackground(Color.WHITE);
		this.deviceSpecific.addItemListener(propertyChangedItemListener);
		ButtonGroup group = new ButtonGroup();
		group.add(this.universal);
		group.add(this.deviceSpecific);

		Vector<String> sizeItems = new Vector<String>();
		sizeItems.add("Any");
		sizeItems.add("Any & Compact");
		sizeItems.add("Any & Regular");
		widthType = new JComboBox(sizeItems);
		widthType.setFont(fontSmall);
		heightType = new JComboBox(sizeItems);
		heightType.setFont(fontSmall);
		settingsNorth.add(widthType);
		settingsNorth.add(heightType);
		settingsNorth.add(widthType);
		settingsNorth.add(heightType);
		settingsNorth.add(widthLabel = new JLabel("Width"));
		settingsNorth.add(heightLabel = new JLabel("Height"));

		// Scaling
		Vector<String> renderItems = new Vector<String>();
		renderItems.add(owner.getResource("splitter.item.render.as.default", "Default"));
		renderItems.add(owner.getResource("splitter.item.render.as.original", "Original"));
		renderItems.add(owner.getResource("splitter.item.render.as.template", "Template"));
		renderAs = new JComboBox(renderItems);
		renderAs.setFont(fontSmall);
		renderAs.addItemListener(propertyChangedItemListener);
		//JLabel renderAsLabel = new JLabel(owner.getResource("splitter.item.render.as", "Render As"));
		//settingsNorth.add(renderAsLabel);
		settingsNorth.add(renderAs);

		SpringLayout layoutNorth = new SpringLayout();
		layoutNorth.putConstraint(SpringLayout.WEST, asImageSets, 8, SpringLayout.WEST, settingsNorth);
		layoutNorth.putConstraint(SpringLayout.NORTH, asImageSets, 8, SpringLayout.NORTH, settingsNorth);
		layoutNorth.putConstraint(SpringLayout.EAST, deviceSpecific, isMacLAF ? -4 : -8, SpringLayout.EAST, settingsNorth);
		layoutNorth.putConstraint(SpringLayout.NORTH, deviceSpecific, 8, SpringLayout.NORTH, settingsNorth);
		layoutNorth.putConstraint(SpringLayout.EAST, universal, -4, SpringLayout.WEST, deviceSpecific);
		layoutNorth.putConstraint(SpringLayout.NORTH, universal, 8, SpringLayout.NORTH, settingsNorth);

		layoutNorth.putConstraint(SpringLayout.WEST, widthLabel, 16, SpringLayout.WEST, settingsNorth);
		layoutNorth.putConstraint(SpringLayout.VERTICAL_CENTER, widthLabel, 0, SpringLayout.VERTICAL_CENTER, widthType);
		layoutNorth.putConstraint(SpringLayout.WEST, widthType, isMacLAF ? 0 : 4, SpringLayout.EAST, widthLabel);
		layoutNorth.putConstraint(SpringLayout.NORTH, widthType, 10, SpringLayout.SOUTH, universal);
		layoutNorth.putConstraint(SpringLayout.WEST, heightLabel, isMacLAF ? 8 : 16, SpringLayout.EAST, widthType);
		layoutNorth.putConstraint(SpringLayout.VERTICAL_CENTER, heightLabel, 0, SpringLayout.VERTICAL_CENTER, heightType);
		layoutNorth.putConstraint(SpringLayout.WEST, heightType, isMacLAF ? 0 : 4, SpringLayout.EAST, heightLabel);
		layoutNorth.putConstraint(SpringLayout.NORTH, heightType, 0, SpringLayout.NORTH, widthType);
		//layoutNorth.putConstraint(SpringLayout.WEST, renderAsLabel, isMacLAF ? 8 : 16, SpringLayout.EAST, heightType);
		//layoutNorth.putConstraint(SpringLayout.VERTICAL_CENTER, renderAsLabel, 0, SpringLayout.VERTICAL_CENTER, renderAs);
		layoutNorth.putConstraint(SpringLayout.EAST, renderAs, isMacLAF ? -4 : -8, SpringLayout.EAST, settingsNorth);
		layoutNorth.putConstraint(SpringLayout.NORTH, renderAs, 0, SpringLayout.NORTH, widthType);

		settingsNorth.setLayout(layoutNorth);
		int preferredWidthNorth = layoutNorth.getConstraints(widthLabel).getWidth().getPreferredValue()
				+ layoutNorth.getConstraints(widthType).getWidth().getPreferredValue()
				+ layoutNorth.getConstraints(heightLabel).getWidth().getPreferredValue()
				+ layoutNorth.getConstraints(heightType).getWidth().getPreferredValue()
		//		+ layoutNorth.getConstraints(renderAsLabel).getWidth().getPreferredValue()
				+ layoutNorth.getConstraints(renderAs).getWidth().getPreferredValue()
				+ (isMacLAF ? 34 : 68);
		int preferredHeightNorth = layoutNorth.getConstraints(widthType).getY().getPreferredValue()
				+ layoutNorth.getConstraints(widthType).getHeight().getPreferredValue()
				+ 12;
		settingsNorth.setPreferredSize(new Dimension(preferredWidthNorth, preferredHeightNorth));
		settings.add(settingsNorth, BorderLayout.NORTH);

		card = new JPanel();
		card.setBackground(Color.WHITE);
		card.setLayout(new CardLayout());
		card.add(sizeDefault = new SplitterSizePanel(SplitterSizePanel.DEVICE_TYPE.DEFAULT, owner), SplitterSizePanel.DEVICE_TYPE.DEFAULT.toString());
		card.add(sizeUniversal = new SplitterSizePanel(SplitterSizePanel.DEVICE_TYPE.UNIVERSAL, owner), SplitterSizePanel.DEVICE_TYPE.UNIVERSAL.toString());
		card.add(deviceSpecificTabs = new JTabbedPane(), SplitterSizePanel.DEVICE_TYPE.IPHONE.toString());
		sizeiPhone = SplitterSizePanel.newAsTab(SplitterSizePanel.DEVICE_TYPE.IPHONE, deviceSpecificTabs, owner);
		sizeiPad = SplitterSizePanel.newAsTab(SplitterSizePanel.DEVICE_TYPE.IPAD, deviceSpecificTabs, owner);
		sizeAppleWatch = SplitterSizePanel.newAsTab(SplitterSizePanel.DEVICE_TYPE.APPLE_WATCH, deviceSpecificTabs, owner);
		sizeMac = SplitterSizePanel.newAsTab(SplitterSizePanel.DEVICE_TYPE.MAC, deviceSpecificTabs, owner);
		settings.add(card, BorderLayout.CENTER);

		widthType.addItemListener(sizeUniversal.createSizeChangedItemListener(owner, widthType, heightType));
		heightType.addItemListener(sizeUniversal.createSizeChangedItemListener(owner, widthType, heightType));
		widthType.addItemListener(sizeiPhone.createSizeChangedItemListener(owner, widthType, heightType));
		heightType.addItemListener(sizeiPhone.createSizeChangedItemListener(owner, widthType, heightType));
		widthType.addItemListener(sizeiPad.createSizeChangedItemListener(owner, widthType, heightType));
		heightType.addItemListener(sizeiPad.createSizeChangedItemListener(owner, widthType, heightType));

		JPanel settingsSouth = new JPanel();
		settingsSouth.setBackground(Color.WHITE);
		
		// Output file type and parameters
		Vector<String> outputAsItems = new Vector<String>();
		outputAsItems.add("PNG");
		for (int i = 1; i < SplitterFrame.COMPRESSION_QUALITIES.length; i++) {
			outputAsItems.add(String.format("JPEG - %d%%", SplitterFrame.COMPRESSION_QUALITIES[i]));
		}
		outputAs = new JComboBox(outputAsItems);
		outputAs.setFont(fontSmall);
		outputAs.addItemListener(propertyChangedItemListener);
		//JLabel renderAsLabel = new JLabel(owner.getResource("splitter.item.render.as", "Render As"));
		//settingsNorth.add(renderAsLabel);
		settingsSouth.add(outputAs);

		// Scaling
		Vector<String> scalingItems = new Vector<String>();
		scalingItems.add(owner.getResource("splitter.item.noresize", "No resizing"));
		scalingItems.add(owner.getResource("splitter.item.fittoshortside", "Fit to short side"));
		scalingItems.add(owner.getResource("splitter.item.fittolongside", "Fit to long side"));
		scalingItems.add(owner.getResource("splitter.item.fittoscreen", "Fit with aspect ratio"));
		scalingItems.add(owner.getResource("splitter.item.noaspectratio", "Fit without aspect ratio"));
		scalingType = new JComboBox(scalingItems);
		scalingType.setToolTipText(owner.getResource("splitter.label.scaling", "Scaling Options"));
		scalingType.setFont(fontSmall);
		scalingType.setSelectedIndex(3);
		scalingType.addItemListener(propertyChangedItemListener);
		settingsSouth.add(scalingType);

		// background color
		final String BGCOLOR_PLACEHOLDER = owner.getResource("splitter.placeholder.transparent", "Transparent");
		bgcolor = new JTextField(8);
		bgcolor.setFont(fontSmall);
		bgcolor.setToolTipText(owner.getResource("tooltip.splash.bgcolor", "tooltip.splash.bgcolor"));
		bgcolor.addFocusListener(new FocusListener() {
			@Override public void focusGained(FocusEvent e) {
				if (bgcolor.getText().trim().equals(BGCOLOR_PLACEHOLDER)) {
					bgcolor.setText("");
				} else {
					bgcolor.selectAll();
				}
			}
			@Override public void focusLost(FocusEvent e) {
				setBackgroundColor(bgcolor.getText());
			}
		});
		JButton bgcolorButton = new JButton("...");
		bgcolorButton.setFont(fontSmall);
		bgcolorButton.addActionListener(new ActionListener() {
			@Override public void actionPerformed(ActionEvent e) {
				Color c = null;
				try {
					c = JColorChooser.showDialog(bgcolor, "Color Chooser", bgcolor.getText().equals(BGCOLOR_PLACEHOLDER) ? null : new Color(Long.valueOf(bgcolor.getText(), 16).intValue(), true));
				} catch (Throwable t) {
					owner.handleThrowable(t);
				}
				if (c != null) {
					setBackgroundColor(String.format("%2h%2h%2h%2h", c.getAlpha(), c.getRed(), c.getGreen(), c.getBlue()).replace(' ', '0'));
				}
			}
		});
		JLabel bgcolorLabel = new JLabel(owner.getResource("splitter.label.bgcolor", "Background Color:"));
		settingsSouth.add(bgcolor);
		settingsSouth.add(bgcolorButton);
		settingsSouth.add(bgcolorLabel);

		settingsSouth.add(this.overwriteAlways = new JCheckBox(owner.getResource("splitter.overwrite.always", "Overwrite always"), false));
		this.overwriteAlways.setToolTipText(owner.getResource("splitter.overwrite.always.tooltip", "Overwrite always"));
		this.overwriteAlways.setBackground(Color.WHITE);
		this.overwriteAlways.addItemListener(propertyChangedItemListener);

		settingsSouth.add(this.cleanBeforeGenerate = new JCheckBox(owner.getResource("splitter.clean.before.generate", "Clean"), false));
		this.cleanBeforeGenerate.setToolTipText(owner.getResource("splitter.clean.before.generate.tooltip", "Clean before generate"));
		this.cleanBeforeGenerate.setBackground(Color.WHITE);
		this.cleanBeforeGenerate.addItemListener(propertyChangedItemListener);

		JLabel subdirLabel = new JLabel(owner.getResource("splitter.label.subdir", "Output Dir :"), SwingConstants.LEFT);
		subdirLabel.setCursor(new Cursor(Cursor.HAND_CURSOR));
		subdirLabel.setToolTipText(owner.getResource("splitter.open.subdir", "Click to open previous target directory."));
		settingsSouth.add(subdirLabel);
		subdirLabel.addMouseListener(new MouseAdapter() {
			@Override public void mouseClicked(MouseEvent e) {
				openOutputDirectory();
			}
		});
		settingsSouth.add(this.subdir = new JTextField(placeHolder, 16));
		Insets insets = new Insets(2, 2, 2, 2);
		this.subdir.setMargin(insets);
		this.subdir.setFont(fontNormal);
		this.subdir.setToolTipText(owner.getResource("splitter.tooltip.subdir", "Same as origin with empty value. Relative and absolute pathes are available."));
		this.subdir.setForeground(Color.GRAY);
		this.subdir.getDocument().addDocumentListener(new DocumentListener() {
			@Override public void changedUpdate(DocumentEvent e) { this.setStorePropertiesRequested(); }
			@Override public void insertUpdate(DocumentEvent e) { this.setStorePropertiesRequested(); }
			@Override public void removeUpdate(DocumentEvent e) { this.setStorePropertiesRequested(); }
			private void setStorePropertiesRequested() {
				if (subdir.getText().trim().isEmpty() || subdir.getText().trim().equals(placeHolder)) {
					return;
				}
				owner.setStorePropertiesRequested(true);
			}
		});
		this.subdir.addFocusListener(new FocusListener() {
			@Override public void focusGained(FocusEvent e) {
				if (subdir.getText().trim().equals(placeHolder)) {
					setOutputDirectory("", true);
				} else {
					subdir.selectAll();
				}
			}
			@Override public void focusLost(FocusEvent e) {
				if (subdir.getText().trim().isEmpty()) {
					setOutputDirectory(placeHolder);
				}
			}
		});
		JButton subdirButton = new JButton("...");
		subdirButton.setFont(fontSmall);
		subdirButton.addActionListener(new ActionListener() {
			@Override public void actionPerformed(ActionEvent e) {
				JFileChooser chooser = new JFileChooser();
				File dir = getPreviousOutputDirectory();
				if (dir != null) {
					chooser.setCurrentDirectory(dir.getParentFile());
				}
				chooser.setApproveButtonText(owner.getResource("button.approve", "Choose"));
				chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
				int returnVal = chooser.showOpenDialog(SplitterFrame.this);
				if(returnVal == JFileChooser.APPROVE_OPTION) {
					setOutputDirectory(chooser.getSelectedFile().getAbsolutePath(), true);
			    }
			}
		});
		settingsSouth.add(subdirButton);

		SpringLayout layoutSouth = new SpringLayout();
		layoutSouth.putConstraint(SpringLayout.WEST, outputAs, 16, SpringLayout.WEST, settingsSouth);
		layoutSouth.putConstraint(SpringLayout.NORTH, outputAs, isMacLAF ? 8 : 10, SpringLayout.NORTH, settingsSouth);
		layoutSouth.putConstraint(SpringLayout.WEST, scalingType, 16, SpringLayout.EAST, outputAs);
		layoutSouth.putConstraint(SpringLayout.VERTICAL_CENTER, scalingType, 0, SpringLayout.VERTICAL_CENTER, outputAs);

		layoutSouth.putConstraint(SpringLayout.EAST, bgcolorLabel, isMacLAF ? 0 : -2, SpringLayout.WEST, bgcolor);
		layoutSouth.putConstraint(SpringLayout.EAST, bgcolor, isMacLAF ? 0 : -4, SpringLayout.WEST, bgcolorButton);
		layoutSouth.putConstraint(SpringLayout.EAST, bgcolorButton, -16, SpringLayout.EAST, settingsSouth);
		layoutSouth.putConstraint(SpringLayout.VERTICAL_CENTER, bgcolor, 0, SpringLayout.VERTICAL_CENTER, scalingType);
		layoutSouth.putConstraint(SpringLayout.VERTICAL_CENTER, bgcolorLabel, 0, SpringLayout.VERTICAL_CENTER, scalingType);
		layoutSouth.putConstraint(SpringLayout.VERTICAL_CENTER, bgcolorButton, 0, SpringLayout.VERTICAL_CENTER, scalingType);

		layoutSouth.putConstraint(SpringLayout.EAST, subdirLabel, isMacLAF ? -2 : -4, SpringLayout.WEST, subdir);
		layoutSouth.putConstraint(SpringLayout.VERTICAL_CENTER, subdirLabel, 0, SpringLayout.VERTICAL_CENTER, subdir);
		layoutSouth.putConstraint(SpringLayout.EAST, subdir, -2, SpringLayout.WEST, subdirButton);
		layoutSouth.putConstraint(SpringLayout.NORTH, subdir, 8, SpringLayout.SOUTH, scalingType);
		layoutSouth.putConstraint(SpringLayout.EAST, subdirButton, -14, SpringLayout.WEST, overwriteAlways);
		layoutSouth.putConstraint(SpringLayout.VERTICAL_CENTER, subdirButton, 0, SpringLayout.VERTICAL_CENTER, subdir);
		layoutSouth.putConstraint(SpringLayout.EAST, overwriteAlways, -8, SpringLayout.WEST, cleanBeforeGenerate);
		layoutSouth.putConstraint(SpringLayout.VERTICAL_CENTER, overwriteAlways, 0, SpringLayout.VERTICAL_CENTER, subdir);
		layoutSouth.putConstraint(SpringLayout.EAST, cleanBeforeGenerate, -16, SpringLayout.EAST, settingsSouth);
		layoutSouth.putConstraint(SpringLayout.VERTICAL_CENTER, cleanBeforeGenerate, 0, SpringLayout.VERTICAL_CENTER, subdir);
		settingsSouth.setLayout(layoutSouth);
		int preferredWidthSouth1 = layoutSouth.getConstraints(subdirLabel).getWidth().getPreferredValue()
				+ layoutSouth.getConstraints(subdir).getWidth().getPreferredValue()
				+ layoutSouth.getConstraints(subdirButton).getWidth().getPreferredValue()
				+ layoutSouth.getConstraints(overwriteAlways).getWidth().getPreferredValue()
				+ layoutSouth.getConstraints(cleanBeforeGenerate).getWidth().getPreferredValue()
				+ (isMacLAF ? 48 : 56);
		int preferredWidthSouth2 = layoutSouth.getConstraints(outputAs).getWidth().getPreferredValue()
				+ layoutSouth.getConstraints(scalingType).getWidth().getPreferredValue()
				+ layoutSouth.getConstraints(bgcolorLabel).getWidth().getPreferredValue()
				+ layoutSouth.getConstraints(bgcolor).getWidth().getPreferredValue()
				+ layoutSouth.getConstraints(bgcolorButton).getWidth().getPreferredValue()
				+ (isMacLAF ? 64 : 72);
		int preferredWidthSouth = preferredWidthSouth1 > preferredWidthSouth2 ? preferredWidthSouth1 : preferredWidthSouth2;
		int preferredHeightSouth = layoutSouth.getConstraints(subdir).getY().getPreferredValue()
				+ layoutSouth.getConstraints(subdir).getHeight().getPreferredValue()
				+ 10;
		settingsSouth.setPreferredSize(new Dimension(preferredWidthSouth, preferredHeightSouth));

		settings.add(settingsSouth, BorderLayout.SOUTH);
		this.add(settings, BorderLayout.NORTH);

		splitButton = new JButton(owner.getResource("splitter.button", "Split"), new ImageIcon(this.getClass().getResource("img/splitter.png")));
		splitButton.setBackground(new Color(0x007AFF));
		splitButton.setForeground(Color.WHITE);
		splitButton.setBorderPainted(false);
		splitButton.setFocusPainted(false);
		splitButton.setDisabledIcon(new ImageIcon(this.getClass().getResource("img/generate.gif")));
		splitButton.setRolloverIcon(new ImageIcon(this.getClass().getResource("img/splitter_rollover.png")));
		splitButton.setHorizontalTextPosition(SwingConstants.CENTER);
		splitButton.setVerticalTextPosition(SwingConstants.BOTTOM);
		splitButton.setFont(fontLarge);
		splitButton.setMargin(new Insets(48, 16, 48, 16));
		splitButton.setOpaque(true);
		//splitButton.setTransferHandler(new TransferHandler() {
		this.setTransferHandler(new TransferHandler() {
			@Override public boolean importData(TransferSupport support) {
				try {
					if (isGenerating()) {
						return false;
					}

					if (canImport(support)) {
						final List<File> files = new LinkedList<File>();
						if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
							Object list = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
							if (list instanceof List<?>) {
								for (Object o : ((List<?>)list)) {
									if (o instanceof File) {
										files.add((File)o);
									}
								}
							}
						} else if (support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
							String urls = (String)support.getTransferable().getTransferData(DataFlavor.stringFlavor);
							StringTokenizer tokens = new StringTokenizer(urls);
							while (tokens.hasMoreTokens()) {
								try {
									File f = new File(URLDecoder.decode((new URL(tokens.nextToken()).getFile()), "UTF-8"));
									if (f.isFile()) {
										files.add(f);
									}
								} catch (Exception ex) {
									System.out.println(ex.getMessage());
								}
			                }
						}
						if (files.size() <= 0) {
							return false;
						}
						setAlwaysOnTop(true);
						toFront();
						requestFocus();
						setAlwaysOnTop(false);
						// JOptionPane.showConfirmDialog has abnormal focus during dropping files on mac. Try another thread.
						(new SwingWorker<Boolean, Integer>() {
							@Override protected Boolean doInBackground() throws Exception {
								try {
									return dropFiles(files);
								} catch (Throwable t) {
									owner.handleThrowable(t);
								}
								return false;
							}
						}).execute();
						return true;
					}
				} catch (Throwable t) {
					owner.handleThrowable(t);
				} finally {
					setAlwaysOnTop(false);
				}
				return false;
			}
			@Override public boolean canImport(TransferSupport support) {
				if (support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
					return true;
				}
				boolean result = false;
				if (support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
					for (DataFlavor flavor : support.getDataFlavors()) {
						if (flavor.getSubType().equals("uri-list")) {
							result = true;
							break;
						}
					}
	            }
				return result;
			}
		});
		splitButton.addActionListener(new ActionListener() {
			@Override public void actionPerformed(ActionEvent e) {
				if (isGenerating()) {
					if (!yesNo(owner.getResource("splitter.question.cancel", "Are you sure to cancel generating images?"))) {
						return;
					}
					try {
						pool.shutdownNow();
					} catch (Throwable t) {
						owner.handleThrowable(t);
					}
					splitButton.setText(owner.getResource("label.cancel.generate", "Cancel generate..."));
					return;
				}

				JFileChooser chooser = new JFileChooser();
				chooser.setFileFilter(new FileNameExtensionFilter("Images (png, jpg, jpeg)", "png", "jpg", "jpeg"));
				chooser.setApproveButtonText(owner.getResource("button.approve", "Choose"));
				chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
				chooser.setMultiSelectionEnabled(true);
				if (previousTargetDirectory == null) {
					previousTargetDirectory = owner.getChosenDirectory();
				}
				if (previousTargetDirectory != null) {
					if (previousTargetDirectory.isFile()) {
						chooser.setCurrentDirectory(previousTargetDirectory.getParentFile());
					} else {
						chooser.setCurrentDirectory(previousTargetDirectory);
					}
				}
				int returnVal = chooser.showOpenDialog(SplitterFrame.this);
				if(returnVal == JFileChooser.APPROVE_OPTION) {
					try {
						List<File> files = new LinkedList<File>();
						for (File f : chooser.getSelectedFiles()) {
							files.add(f);
						}
						dropFiles(files);
					} catch (Throwable t) {
						owner.handleThrowable(t);
					}
			    }

			}
		});
		this.add(splitButton, BorderLayout.CENTER);

		progress = new JProgressBar();
		progress.setValue(0);
		progress.setStringPainted(true);
		progress.setBorderPainted(false);
		progress.setBackground(isMacLAF ? Color.WHITE : MainFrame.BGCOLOR_LIGHT_GRAY);
		progress.setForeground(splitButton.getBackground());
		progress.setOpaque(true);
		this.add(progress, BorderLayout.SOUTH);

		this.addWindowListener(new WindowAdapter() {
			@Override public void windowClosing(WindowEvent e) {
				if (owner.isBatchMode()) {
					return;
				}
				if (isGenerating()) {
					setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
					alert(owner.getResource("error.close.window", "Please wait until generating process will be finished."));
					return;
				}
				setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
			}
			@Override public void windowOpened(WindowEvent e) {
				// Swing components sometimes will not be drawn on some graphics borad. Hack it.
				setSize(getSize().width + 1, getSize().height + 1);
				pack();
				repaint();	// This code does not need, maybe.
			}
		});
		this.pack();
		this.applyComponentStatus();
	}
	
	/**
	 * Open previous target directory.
	 */
	protected void openOutputDirectory() {
		File dir = this.getPreviousOutputDirectory();
		if (dir != null) {
			try {
				Desktop.getDesktop().open(dir);
			} catch (Throwable t) {
				owner.handleThrowable(t);
			}
		}
	}
	
	protected File getPreviousOutputDirectory() {
		File dir = previousOutputDirectory;
		if (dir == null && !IOSImageUtil.isNullOrWhiteSpace(subdir.getText())) {
			dir = new File(subdir.getText());
			if (!dir.isAbsolute()) {
				dir = null;
			}
		}
		if (dir == null && previousTargetDirectory != null) {
			dir = previousTargetDirectory;
			if (!IOSImageUtil.isNullOrWhiteSpace(subdir.getText())) {
				dir = new File(dir, subdir.getText());
			}
		}
		if (dir != null && dir.exists()) {
			return dir;
		}
		return null;
	}

	/**
	 * true - Generating images just now.
	 *
	 * @return
	 */
	protected boolean isGenerating() {
		return (pool != null && !pool.isTerminated());
	}

	/**
	 * Process dropped files.
	 *
	 * @param files
	 * @return
	 */
	protected boolean dropFiles(List<File> files) {
		try {
			if (this.isGenerating()) {
				return false;
			}

			contentCache.clear();
			LinkedList<IOSImageSet> queue = new LinkedList<IOSImageSet>();
			HashMap<String, File> deleteTargets = new HashMap<String, File>();
			for (File file : files) {
				if (!file.exists()) {
					alert("[" + file.getCanonicalPath() + "] " + owner.getResource("error.not.exists", "is not exists."));
					continue;
				}
				if (!file.isFile()) {
					alert("[" + file.getCanonicalPath() + "] " + owner.getResource("error.not.file", "is directory. Choose file."));
					continue;
				}
				previousTargetDirectory = file.getParentFile();
				File dir = file.getParentFile();
				String sub = this.subdir.getText().trim();
				if (sub.equals(owner.getResource("splitter.label", "Same as origin."))) {
					sub = "";
				}
				if (!sub.isEmpty()) {
					File subPath = new File(sub);
					if (subPath.isAbsolute()) {
						// absolute path.
						dir = subPath;
						if (!dir.exists() || !dir.isDirectory()) {
							alert(owner.getResource("splitter.error.subdir", "Only existance directory is available with absolute path."));
							return false;
						}
					} else {
						// relative path.
						dir = new File(dir, sub);
						if (!dir.exists()) {
							dir.mkdirs();
						}
					}
				}
				previousOutputDirectory = dir;
				String suffix = this.getOutputSuffix();
				if (this.asImageSets.isSelected()) {
					if (this.universal.isSelected()) {
						queue = this.sizeUniversal.addImageQueue(owner, queue, file, dir, suffix);
					} else {
						queue = this.sizeiPhone.addImageQueue(owner, queue, file, dir, suffix);
						queue = this.sizeiPad.addImageQueue(owner, queue, file, dir, suffix);
						queue = this.sizeAppleWatch.addImageQueue(owner, queue, file, dir, suffix);
						queue = this.sizeMac.addImageQueue(owner, queue, file, dir, suffix);
					}
					if (this.cleanBeforeGenerate.isSelected() && queue.size() > 0) {
						IOSImageSet imageSet = queue.peek();
						deleteTargets.put(imageSet.getImageName(), imageSet.getFile().getParentFile());
					}
				} else {
					queue = this.sizeDefault.addImageQueue(owner, queue, file, dir, suffix);
				}
			}

			if (!owner.isBatchMode() && !this.overwriteAlways.isSelected()) {
				String existingFilename = null;
				for (IOSImageSet imageSets : queue) {
					if (imageSets.getFile().exists()) {
						existingFilename = imageSets.getFile().getAbsolutePath();
						break;
					}
				}
				if (existingFilename != null) {
					if (!yesNo(String.format(owner.getResource("splitter.already.exists", "[%s] already exists. Replace file?"), existingFilename))) {
						return false;
					}
				}
			}

			if (this.asImageSets.isSelected() && this.cleanBeforeGenerate.isSelected() && deleteTargets.size() > 0) {
				for (Map.Entry<String, File>keyValue : deleteTargets.entrySet()) {
					File targetDirectory = keyValue.getValue();
					final String orgFilename = keyValue.getKey();
					String[] filenames = targetDirectory.list(new FilenameFilter() {
						@Override public boolean accept(File dir, String name) {
							if (!IOSImageUtil.isNullOrWhiteSpace(name)) {
								if (name.toLowerCase().trim().matches(String.format("^%s-(iphone|ipad|mac|watch|universal)+(-compact(w|h)|-regular(w|h)|-retina4|-38mm|-42mm)*(-compact(w|h)|-regular(w|h)|-retina4)*(-compact(w|h)|-regular(w|h)|-retina4)*@(1|2|3)x\\.png$", orgFilename))) {
									return true;
								}
							}
							return false;
						}
					});
					if (filenames != null) {
						for (String filename : filenames) {
							File f = new File(targetDirectory, filename);
							if (f.exists() && f.isFile()) {
								if (!f.delete()) {
									System.err.println("Could not delete file - " + f.getAbsolutePath());
								}
							}
						}
					}
				}
			}

			progress.setMaximum(queue.size());
			progress.setValue(0);
			if (owner.isBatchMode()) {
				if (!owner.isSilentMode() && !owner.isVerboseMode()) {
					System.out.println("Split Images:");
					for (int i = 0; i < progress.getMaximum(); i++) {
						System.out.print(".");
					}
				}
				System.out.print((char)0x0D);
			} else {
				this.enableGUI(false);
			}

			pool = Executors.newFixedThreadPool(Math.min(IOSImageUtil.getSystemIntProperty("org.gootara.ios.image.util.max.threads", 12), Math.max(IOSImageUtil.getSystemIntProperty("org.gootara.ios.image.util.max.threads", 4), Runtime.getRuntime().availableProcessors())));
			IOSImageSet imageSets;
			while ((imageSets = queue.poll()) != null) {
				if (!imageSets.getOriginalFile().exists()) {
					alert("[" + imageSets.getFile().getCanonicalPath() + "] " + owner.getResource("error.not.exists", "is not exists."));
					this.addProgress(false);
					continue;
				}
				split(imageSets);
			}

			SwingWorker<Boolean, Integer> watchdog = new SwingWorker<Boolean, Integer>() {
				@Override protected Boolean doInBackground() throws Exception {
					boolean result = true;
					try {
						pool.shutdown();
						if (pool.awaitTermination(progress.getMaximum(), TimeUnit.MINUTES)) {
							if (asImageSets.isSelected()) {
								for (ArrayList<IOSImageSet> list : contentCache.values()) {
									if (list.size() <= 0) {
										continue;
									}

									Collections.sort(list);
									IOSAssetCatalogs.JsonUtility jsonUtility = new IOSAssetCatalogs.JsonUtility();
									jsonUtility.addAll(list);
									if (renderAs.getSelectedIndex() != 0) {
										jsonUtility.setInfo(IOSImageSet.JSON_INFO_KEY.RENDERING, renderAs.getSelectedIndex() == 2 ? IOSImageSet.JSON_INFO_VALUE.RENDERING_TEMPLATE : IOSImageSet.JSON_INFO_VALUE.RENDERING_ORIGINAL);
									}

									File f = new File(list.get(0).getFile().getParentFile(), owner.getResource("string.filename.contents.json", "Contents.json"));
									jsonUtility.writeContentJson(f);
									list.clear();
								}
							}
						} else {
							result = false;
						}
					} catch (Throwable t) {
						owner.handleThrowable(t);
					} finally {
						if (owner.isBatchMode()) {
							if (!owner.isSilentMode() && !owner.isVerboseMode()) {
								System.out.println();
								System.out.println();
							}
						} else {
							SplitterFrame.this.enableGUI(true);
						}
						contentCache.clear();
						System.gc();
						progress.setValue(0);
					}
					return result;
				}
			};
			watchdog.execute();
			if (owner.isBatchMode()) {
				return watchdog.get();
			}
		} catch (Throwable t) {
			owner.handleThrowable(t);
			if (owner.isBatchMode()) {
				if (!owner.isSilentMode() && !owner.isVerboseMode()) {
					System.out.println();
					System.out.println();
				}
			} else {
				this.enableGUI(true);
			}
			return false;
		}
		return true;
	}

	/**
	 * Set gui components availability.
	 *
	 * @param b
	 */
	private void enableGUI(boolean b) {
		if (owner.isBatchMode()) {
			return;
		}

		splitButton.setText(b ? owner.getResource("splitter.button", "Split") : owner.getResource("splitter.executing", "Split images..."));
		splitButton.setBackground(b ? new Color(0x007AFF) : new Color(0xF7F7F7));
		splitButton.setForeground(b ? Color.WHITE : Color.GRAY);
		splitButton.setIcon(new ImageIcon(this.getClass().getResource(b ? "img/splitter.png" : "img/generate.gif")));
		splitButton.setRolloverIcon(new ImageIcon(this.getClass().getResource(b ? "img/splitter_rollover.png" : "img/generate.gif")));
		splitButton.setEnabled(b);
		setContainerEnabled(settings, b);
		if (b) {
			this.applyComponentStatus();
		}
	}

	/**
	 * Set container enabled.
	 *
	 * @param parent
	 * @param b
	 */
	protected static void setContainerEnabled(Container parent, boolean b) {
		for (Component c : parent.getComponents()) {
			if (c instanceof JTextField) {
				((JTextField)c).setEditable(b);
			} else {
				c.setEnabled(b);
			}
			if (c instanceof Container) {
				setContainerEnabled((Container)c, b);
			}
		}
	}

	/**
	 * Add progress.
	 */
	private void addProgress(boolean b) {
		if (owner.isBatchMode()) {
			if (!owner.isSilentMode() && !owner.isVerboseMode()) {
				System.out.print(b ? "o" : "-");
			}
		} else {
			progress.setValue(progress.getValue() + 1);
		}
	}

	/**
	 * Apply properties.
	 *
	 * @param props properties
	 * @return same as props
	 */
	protected Properties applyProperties(Properties props, boolean system) {
		Properties def = this.getDefaultProperties(new Properties());
		this.setAsImageSets(owner.getBoolProperty(props, "splitter.as.imagesets.selected", def));
		this.setUniversal(owner.getBoolProperty(props, "splitter.universal.selected", def));
		this.setDeviceSpecific(owner.getBoolProperty(props, "splitter.device.specific.selected", def));
		this.setWidthType(owner.getIntProperty(props, "splitter.width.type", def));
		this.setHeightType(owner.getIntProperty(props, "splitter.height.type", def));
		this.setRenderAs(owner.getIntProperty(props, "splitter.render.as", def));

		this.setOverwriteAlways(owner.getBoolProperty(props, "splitter.overwrite.always.selected", def));
		this.setCleanBeforeGenerate(owner.getBoolProperty(props, "splitter.clean.before.generate.selected", def));
		this.setOutputDirectory(props.getProperty("splitter.sub.directory", "").trim().equals(owner.getResource("splitter.label", "Same as origin.")) ? "" : owner.getStringProperty(props, "splitter.sub.directory", def));
		if (owner.getBoolProperty(props, "splitter.as3x.selected", def)) {
			information(owner.getResource("splitter.info.as3x", "'splitter.as3x.selected' property was deprecated. Use 100% size instead."));
			this.setWidth1x("100 %");
			this.setHeight1x("100 %");
		}
		this.setBackgroundColor(owner.getStringProperty(props, "splitter.image.bgcolor", def));
		this.setScalingType(owner.getIntProperty(props, "splitter.scaling.type", def));
		this.setOutputAs(owner.getIntProperty(props, "splitter.output.as", def));

		if (system) {
			String lastTargetDirectory = props.getProperty("system.splitter.last.target.directory", "").trim();
			if (!lastTargetDirectory.isEmpty()) {
				previousTargetDirectory = new File(lastTargetDirectory);
				if (!previousTargetDirectory.exists() || !previousTargetDirectory.isDirectory()) {
					previousTargetDirectory = null;
				}
			} else {
				previousTargetDirectory = null;
			}
		}
		props = this.sizeDefault.applyProperties(owner, props);
		props = this.sizeUniversal.applyProperties(owner, props);
		props = this.sizeiPhone.applyProperties(owner, props);
		props = this.sizeiPad.applyProperties(owner, props);
		props = this.sizeAppleWatch.applyProperties(owner, props);
		props = this.sizeMac.applyProperties(owner, props);
		return props;
	}

	/**
	 * Store properties.
	 *
	 * @param props properties
	 * @return same as props
	 */
	protected Properties storeProperties(Properties props, boolean system) {
		props.put("splitter.overwrite.always.selected", Boolean.toString(this.overwriteAlways.isSelected()));
		props.put("splitter.clean.before.generate.selected", Boolean.toString(this.cleanBeforeGenerate.isSelected()));
		props.put("splitter.sub.directory", this.getOutputDirectory());
		props.put("splitter.as.imagesets.selected", Boolean.toString(this.asImageSets.isSelected()));
		props.put("splitter.universal.selected", Boolean.toString(this.universal.isSelected()));
		props.put("splitter.device.specific.selected", Boolean.toString(this.deviceSpecific.isSelected()));
		props.put("splitter.width.type", Integer.toString(this.widthType.getSelectedIndex()));
		props.put("splitter.height.type", Integer.toString(this.heightType.getSelectedIndex()));
		props.put("splitter.render.as", Integer.toString(this.renderAs.getSelectedIndex()));
		props.put("splitter.image.bgcolor", this.getBackgroundColor());
		props.put("splitter.scaling.type", Integer.toString(this.scalingType.getSelectedIndex()));
		props.put("splitter.output.as", Integer.toString(this.outputAs.getSelectedIndex()));
		if (system) {
			try {
				props.put("system.splitter.last.target.directory", previousTargetDirectory == null ? "" : previousTargetDirectory.getCanonicalPath());
			} catch (Exception ex) {
				ex.printStackTrace(); props.put("system.splitter.last.target.directory", "");
			}
		}
		props = this.sizeDefault.storeProperties(props);
		props = this.sizeUniversal.storeProperties(props);
		props = this.sizeiPhone.storeProperties(props);
		props = this.sizeiPad.storeProperties(props);
		props = this.sizeAppleWatch.storeProperties(props);
		props = this.sizeMac.storeProperties(props);
		return props;
	}

	/**
	 * Put default properties.
	 *
	 * @param props
	 * @return
	 */
	protected Properties getDefaultProperties(Properties props) {
		props.put("splitter.width1x", "44 px");
		props.put("splitter.height1x", "44 px");
		props.put("splitter.overwrite.always.selected", Boolean.toString(false));
		props.put("splitter.clean.before.generate.selected", Boolean.toString(true));
		props.put("splitter.sub.directory", "");
		props.put("splitter.as3x.selected", (new Boolean(false)).toString());
		props.put("splitter.as.imagesets.selected", (new Boolean(false)).toString());
		props.put("splitter.universal.selected", (new Boolean(true)).toString());
		props.put("splitter.device.specific.selected", (new Boolean(false)).toString());
		props.put("splitter.width.type", Integer.toString(0));
		props.put("splitter.height.type", Integer.toString(0));
		props.put("splitter.image.bgcolor", "");
		props.put("splitter.scaling.type", Integer.toString(3));
		props.put("splitter.render.as", Integer.toString(0));
		props.put("splitter.output.as", Integer.toString(0));
		props = this.sizeDefault.getDefaultProperties(props);
		props = this.sizeUniversal.getDefaultProperties(props);
		props = this.sizeiPhone.getDefaultProperties(props);
		props = this.sizeiPad.getDefaultProperties(props);
		props = this.sizeAppleWatch.getDefaultProperties(props);
		props = this.sizeMac.getDefaultProperties(props);
		return props;
	}

	// Options
	private void setAsImageSets(boolean b) { this.asImageSets.setSelected(b); this.applyComponentStatus(); }
	private void setUniversal(boolean b) { this.universal.setSelected(b); this.applyComponentStatus(); }
	private void setDeviceSpecific(boolean b) { this.deviceSpecific.setSelected(b); this.applyComponentStatus(); }
	private void setWidthType(int index) { this.widthType.setSelectedIndex(index); this.applyComponentStatus(); }
	private void setHeightType(int index) { this.heightType.setSelectedIndex(index); this.applyComponentStatus(); }
	private void setRenderAs(int index) { this.renderAs.setSelectedIndex(index); }

	protected void setWidth1x(String width) { sizeDefault.setWidthAny(width); }
	protected void setHeight1x(String height) { sizeDefault.setHeightAny(height); }
	protected void setOverwriteAlways(boolean b) { this.overwriteAlways.setSelected(b); }
	protected void setCleanBeforeGenerate(boolean b) { this.cleanBeforeGenerate.setSelected(b); }
	protected void setOutputDirectory(String relativePath) { this.setOutputDirectory(relativePath, false); }
	private void setOutputDirectory(String relativePath, boolean focusGained) {
		String placeHolder = owner.getResource("splitter.label", "Same as origin.");
		if (IOSImageUtil.isNullOrWhiteSpace(relativePath) || relativePath.equals(placeHolder)) {
			subdir.setText(focusGained ? "" : placeHolder);
			subdir.setForeground(focusGained ? Color.BLACK : MainFrame.COLOR_GRAY);
		} else {
			this.subdir.setText(relativePath);
			subdir.setForeground(Color.BLACK);
		}
	}
	protected void setScalingType(int index) { scalingType.setSelectedIndex(index); }
	private void setOutputAs(int index) { this.outputAs.setSelectedIndex(index); }
	private String getOutputSuffix() { return this.outputAs.getSelectedIndex() > 0 ? "jpg" : "png"; }

	/**
	 * Get output directory
	 *
	 * @return
	 */
	private String getOutputDirectory() {
		if (subdir.getText().trim().isEmpty() || owner.getResource("splitter.label", "Same as origin.").equals(subdir.getText())) {
			return new String();
		}
		return subdir.getText();
	}

	/**
	 * Set image background color.
	 *
	 * @param s color string
	 */
	public void setBackgroundColor(String s) {
		final String BGCOLOR_PLACEHOLDER = owner.getResource("splitter.placeholder.transparent", "Transparent");
		String prevText = this.bgcolor.getText().trim().isEmpty() ? BGCOLOR_PLACEHOLDER : this.bgcolor.getText();
		Color col = null;
		if (!IOSImageUtil.isNullOrWhiteSpace(s)) {
			while (s.length() < 6) s = "0".concat(s);
			if (s.length() > 8) s = s.substring(0, 8);
			if (s.length() == 7) s = "0".concat(s);
			try {
				col = new Color(Long.valueOf(s, 16).intValue(), true);
				bgcolor.setText(s);
				bgcolor.setBackground(new Color(col.getRed(), col.getGreen(), col.getBlue()));
				float[] hsb = Color.RGBtoHSB(col.getRed(), col.getGreen(), col.getBlue(), null);
				//bgcolor.setForeground(new Color(Color.HSBtoRGB(hsb[0], hsb[1], (float)Math.floor(Math.acos(hsb[2])))));
				bgcolor.setForeground(new Color(Color.HSBtoRGB(hsb[0], 1.0f - hsb[1], 1.0f - hsb[2])));
			} catch (Exception ex) {
				bgcolor.setBackground(Color.WHITE);
				bgcolor.setForeground(Color.LIGHT_GRAY);
				ex.printStackTrace();
			}
		}
		if (col == null) {
			bgcolor.setText(BGCOLOR_PLACEHOLDER);
			bgcolor.setForeground(Color.LIGHT_GRAY);
			bgcolor.setBackground(Color.WHITE);
		}
		if (!prevText.equals(this.bgcolor.getText())) {
			owner.setStorePropertiesRequested(true);
		}
	}

	/**
	 * Get background color text.
	 *
	 * @return background color text
	 */
	public String getBackgroundColor() {
		final String BGCOLOR_PLACEHOLDER = owner.getResource("splitter.placeholder.transparent", "Transparent");
		if (bgcolor.getText().trim().isEmpty() || (BGCOLOR_PLACEHOLDER).equals(bgcolor.getText())) {
			return new String();
		}
		return bgcolor.getText();
	}

	/**
	 * Get image background color.
	 *
	 * @return
	 */
	public Color getImageBackgroundColor() {
		try {
			if (!this.getBackgroundColor().isEmpty()) {
				return new Color(Long.valueOf(this.getBackgroundColor(), 16).intValue(), true);
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return null;
	}

	/**
	 * Apply components status by item selections.
	 */
	private void applyComponentStatus() {
		this.universal.setVisible(this.asImageSets.isSelected());
		this.deviceSpecific.setVisible(this.asImageSets.isSelected());
		if (this.asImageSets.isSelected()) {
			((CardLayout)card.getLayout()).show(card, this.universal.isSelected() ? SplitterSizePanel.DEVICE_TYPE.UNIVERSAL.toString() : SplitterSizePanel.DEVICE_TYPE.IPHONE.toString());
		} else {
			((CardLayout)card.getLayout()).show(card, SplitterSizePanel.DEVICE_TYPE.DEFAULT.toString());
		}
		this.widthType.setVisible(this.asImageSets.isSelected());
		this.heightType.setVisible(this.asImageSets.isSelected());
		this.widthLabel.setVisible(this.asImageSets.isSelected());
		this.heightLabel.setVisible(this.asImageSets.isSelected());
		this.renderAs.setVisible(this.asImageSets.isSelected());

		this.cleanBeforeGenerate.setEnabled(this.asImageSets.isSelected());

		SplitterSizePanel[] sizes = { this.sizeDefault, this.sizeUniversal, this.sizeiPhone, this.sizeiPad, this.sizeAppleWatch, this.sizeMac };
		for (SplitterSizePanel p : sizes) {
			p.applyComponentStatus();
		}
	}

	/**
	 * Do split images with scale.
	 *
	 * @param srcFile	image file
	 * @param src		buffer
	 * @param scale		scale
	 * @throws Exception
	 */
	private void split(final IOSImageSet imageSet) {
		SwingWorker<Boolean, Integer> worker = new SwingWorker<Boolean, Integer>() {
			@Override protected Boolean doInBackground() throws Exception {
				boolean result = false;
				try {
					BufferedImage src = ImageIO.read(imageSet.getOriginalFile());
					if (src == null) {
						throw new Exception(owner.getResource("error.not.supported", "Unsupported file type."));
					}
					
					if (!imageSet.getFile().getParentFile().exists()) {
						imageSet.getFile().getParentFile().mkdirs();
					}
					boolean writable = (imageSet.getFile().exists() && imageSet.getFile().canWrite());
					if (!writable && !imageSet.getFile().exists()) {
						try { writable = imageSet.getFile().createNewFile(); } catch (Exception ex) { writable = false; }
					}
					if (!writable) {
						throw new IOException(String.format("%s [%s]", owner.getResource("error.not.writable", "not writable"), imageSet.getFile().getAbsolutePath()));
					}
					owner.verbose(imageSet.getFile());

					Dimension imageSize = new Dimension(src.getWidth(), src.getHeight());

					// width / height @1x.
					double outputWidth1x = SplitterSizePanel.getImageSize(imageSet.getWidthAsString(), imageSize.getWidth());
					double outputHeight1x = SplitterSizePanel.getImageSize(imageSet.getHeightAsString(), imageSize.getHeight());

					// Rescue empty width / height.
					if (outputWidth1x == 0) {
						outputWidth1x = imageSize.getWidth() * (outputHeight1x / imageSize.getHeight());
					}
					if (outputHeight1x == 0) {
						outputHeight1x = imageSize.getHeight() * (outputWidth1x / imageSize.getWidth());
					}

					Dimension outputSize = new Dimension();
					outputSize.setSize(outputWidth1x * imageSet.getScale().value(), outputHeight1x * imageSet.getScale().value());
					imageSet.setSize(outputSize);
					BufferedImage  buf = owner.createBufferedImage(src, outputSize.width, outputSize.height, !imageSet.getSuffix().equalsIgnoreCase("png"));
					Graphics g = buf.getGraphics();
					Color backgroundColor =  getImageBackgroundColor();
					if (backgroundColor == null && imageSet.getSuffix().equalsIgnoreCase("jpg")) {
						backgroundColor = Color.white;
					}
					if (backgroundColor != null) {
						g.setColor(backgroundColor);
						g.fillRect(0, 0, outputSize.width, outputSize.height);
					}
					double percentage = 1.0d;
					if (scalingType.getSelectedIndex() == 1) {
						// Fit to short side
						percentage = outputSize.getWidth() > outputSize.getHeight() ? outputSize.getHeight() / imageSize.getHeight() : outputSize.getWidth() / imageSize.getWidth();
					} else if (scalingType.getSelectedIndex() == 2) {
						// Fit to long side
						percentage = outputSize.getWidth() < outputSize.getHeight() ? outputSize.getHeight() / imageSize.getHeight() : outputSize.getWidth() / imageSize.getWidth();
					} else if (scalingType.getSelectedIndex() == 3) {
						// Fit with aspect ratio
						if (outputSize.getWidth() / outputSize.getHeight() == imageSize.getWidth() / imageSize.getHeight()) {
							imageSize.setSize(outputSize);
						} else {
							percentage = outputSize.getWidth() / imageSize.getWidth();
							if (Math.floor(imageSize.getWidth() * percentage) > outputSize.getWidth() || Math.floor(imageSize.getHeight() * percentage) > outputSize.getHeight()) {
								percentage = outputSize.getHeight() / imageSize.getHeight();
							}
							/*
							percentage = outputSize.getWidth() > outputSize.getHeight() ? outputSize.getHeight() / imageSize.getHeight() : outputSize.getWidth() / imageSize.getWidth();
							if (Math.ceil(imageSize.getWidth() * percentage) > outputSize.getWidth() || Math.ceil(imageSize.getHeight() * percentage) > outputSize.getHeight()) {
								percentage = outputSize.getWidth() < outputSize.getHeight() ? outputSize.getHeight() / imageSize.getHeight() : outputSize.getWidth() / imageSize.getWidth();
							}
							*/
						}
					} else if (scalingType.getSelectedIndex() == 4) {
						// Fill without aspect ratio
						imageSize.setSize(outputSize);
					}
					if (percentage != 1.0d) {
						imageSize.setSize(imageSize.getWidth() * percentage, imageSize.getHeight() * percentage);
					}
					Image img = src.getScaledInstance(imageSize.width, imageSize.height, owner.getScalingAlgorithm());
					g.drawImage(img, (int)((outputSize.getWidth() - imageSize.getWidth()) / 2.0), (int)((outputSize.getHeight() - imageSize.getHeight()) / 2.0), SplitterFrame.this);
					img.flush();
					img = null;
					
					// Image Set image type is always ARGB.
					if (imageSet.getSuffix().equalsIgnoreCase("png") || outputAs.getSelectedIndex() <= 0) {
						ImageIO.write(owner.fixImageColor(buf, src), imageSet.getSuffix(), imageSet.getFile());
					} else {
						ImageWriter writer = ImageIO.getImageWritersBySuffix(imageSet.getSuffix()).next();
						BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(imageSet.getFile()));
						ImageOutputStream outs = ImageIO.createImageOutputStream(bout);
						writer.setOutput(outs);
						ImageWriteParam param = writer.getDefaultWriteParam();
						param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
						param.setCompressionQuality((float)COMPRESSION_QUALITIES[outputAs.getSelectedIndex()] / 100f);
						writer.write(null, new IIOImage(buf, null, null), param);
						bout.flush();
						outs.close();
						bout.close();
						writer.dispose();
						writer = null;
					}
					buf.flush();
					buf = null;
					src.flush();
					src = null;
					addContentCache(imageSet);
					result = true;
				} catch (OutOfMemoryError err) {
					owner.handleThrowable(err);
				} catch (Throwable t) {
					owner.handleThrowable(new Throwable(imageSet.getOriginalFile().getAbsolutePath() + "\n" + (t.getLocalizedMessage() == null ? t.toString() : t.getLocalizedMessage()), t));
				} finally {
					addProgress(result);
				}
				return result;
			}
		};
		pool.submit(worker);
	}

	private synchronized void addContentCache(IOSImageSet imageSet) {
		ArrayList<IOSImageSet> list = contentCache.get(imageSet.getOriginalFile());
		if (list == null) {
			contentCache.put(imageSet.getOriginalFile(), list = new ArrayList<IOSImageSet>());
		}
		list.add(imageSet);
	}


	/**
	 * Show information message.
	 *
	 * @param message	message
	 */
	private void information(String message) {
		owner.information(this, message);
	}

	/**
	 * Show alert.
	 *
	 * @param message	alert message
	 */
	private void alert(String message) {
		owner.alert(this, message);
	}

	/**
	 * Show confirm.
	 * Always 'true' with the batch mode.
	 *
	 * @param message	message
	 * @return	true - ok / false - cancel
	 */
	private boolean confirm(String message) {
		return owner.confirm(this, message);
	}

	/**
	 * Show yes / no dialog.
	 * Always 'false' with the batch mode.
	 *
	 * @param message	message
	 * @return true - yes / false - no
	 */
	private boolean yesNo(String message) {
		return owner.yesNo(this, message);
	}
}



class SplitterSizePanel extends JPanel
{
	public static enum DEVICE_TYPE {
			DEFAULT     ("Default")
		,	UNIVERSAL   ("Universal")
		,	IPHONE      ("iPhone")
		,	IPAD        ("iPad")
		,	APPLE_WATCH ("AppleWatch")
		,	MAC         ("Mac")
		,	CARPLAY     ("CarPlay")
		;
		private final String value;
		private DEVICE_TYPE(final String value) {
			this.value = value;
		}
		public String toString() {
			return this.value;
		}
	};

	private JPanel settings;
	private JTextField widthAny, heightAny, width38mm, height38mm, width42mm, height42mm;
	private JTextField[] widthTraits = { null, null, null };
	private JTextField[] heightTraits = { null, null, null };
	private JCheckBox generateTarget, generateRetina4;
	private JComboBox traits;
	private ImageIcon iconChecked, iconUnchecked;
	private SplitterSizePanel.DEVICE_TYPE type;
	private JTabbedPane tabs;
	private int widthType, heightType;

	public SplitterSizePanel(final SplitterSizePanel.DEVICE_TYPE type, final MainFrame owner) {
		this.type = type;
		widthType = heightType = 0;
		Font fontLarge = new Font(owner.getResource("font.default.name", Font.SANS_SERIF), Font.BOLD, type == DEVICE_TYPE.DEFAULT || type == DEVICE_TYPE.UNIVERSAL ? 14 : 12);
		Font fontSmall = new Font(owner.getResource("font.default.name", Font.SANS_SERIF), Font.PLAIN, 11);
		iconChecked = new ImageIcon(this.getClass().getResource("img/checked.png"));
		iconUnchecked = new ImageIcon(this.getClass().getResource("img/unchecked.png"));
		boolean isMacLAF = UIManager.getLookAndFeel().getName().toLowerCase().indexOf("mac") >= 0;
		DocumentListener documentListener = (new DocumentListener() {
			@Override public void changedUpdate(DocumentEvent e) { owner.setStorePropertiesRequested(true); }
			@Override public void insertUpdate(DocumentEvent e) { owner.setStorePropertiesRequested(true); }
			@Override public void removeUpdate(DocumentEvent e) { owner.setStorePropertiesRequested(true); }
		});

		this.setLayout(new BorderLayout());

		settings = new JPanel();

		JPanel northPane = new JPanel();
		northPane.setLayout(new GridLayout(1, 2));
		if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
			northPane.setBorder(new EmptyBorder(4, 8, 0, 8));
		} else {
			northPane.setBorder(new EmptyBorder(4, 8, 2, 8));
		}
		northPane.add(this.generateTarget = new JCheckBox(owner.getResource("splitter.generate.target", "Generage Images"), false));
		this.generateTarget.setHorizontalAlignment(SwingConstants.LEFT);
		this.generateTarget.addItemListener(new ItemListener() {
			@Override public void itemStateChanged(ItemEvent e) {
				if (e.getSource() instanceof JCheckBox) {
					owner.setStorePropertiesRequested(true);
					applyComponentStatus();
				}
			}
		});
		northPane.add(this.generateRetina4 = new JCheckBox(owner.getResource("splitter.generate.retina4", "Generage Retina4"), false));
		this.generateRetina4.addItemListener(new ItemListener() {
			@Override public void itemStateChanged(ItemEvent e) {
				if (e.getSource() instanceof JCheckBox) {
					owner.setStorePropertiesRequested(true);
				}
			}
		});
		this.generateRetina4.setHorizontalAlignment(SwingConstants.RIGHT);
		this.generateRetina4.setToolTipText(owner.getResource("splitter.generate.retina4.tooltip", "Same as @2x."));

		JLabel labelSized = new JLabel(owner.getResource("splitter.label.info", "Tooltip will be shown with pointing mouse on TextField."));
		JLabel labelWidth = new JLabel(owner.getResource("splitter.label.width", "Width"));
		JLabel labelHeight = new JLabel(owner.getResource("splitter.label.height", "Height"));
		JLabel labelAny = new JLabel("Any");
		settings.add(labelSized);
		settings.add(labelWidth);
		settings.add(labelHeight);
		labelSized.setFont(fontSmall);
		labelSized.setForeground(MainFrame.COLOR_DARK_GRAY);// Color(0x8E8E93));
		labelWidth.setFont(fontLarge);
		labelHeight.setFont(fontLarge);

		FocusListener focusListener = (new FocusListener() {
			@Override public void focusGained(FocusEvent e) {
				if (e.getSource() instanceof JTextField) {
					JTextField text = (JTextField)e.getSource();
					text.select(0, text.getText().indexOf(" ") < 0 ? text.getText().length() : text.getText().indexOf(" "));
				}
			}
			@Override public void focusLost(FocusEvent e) {
				if (e.getSource() instanceof JTextField) {
					JTextField text = (JTextField)e.getSource();
					setSizeText(text, text.getText());
				}
			}
		});
		settings.add(this.widthAny = this.createSizeText(owner.getResource("splitter.label.sized", "Syntax: [@1x px] or [@3x %]"), focusListener, documentListener));
		settings.add(this.heightAny = this.createSizeText(owner.getResource("splitter.label.sized", "Syntax: [@1x px] or [@3x %]"), focusListener, documentListener));

		if (this.getType() != DEVICE_TYPE.DEFAULT) {
			settings.add(labelAny);
		}
		if (this.isSupprtSizeClasses()) {
			traits = new JComboBox();
			traits.setAlignmentX(RIGHT_ALIGNMENT);
			traits.setFont(new Font(owner.getResource("font.default.name", Font.SANS_SERIF), Font.PLAIN, 10));
			traits.addItemListener(new ItemListener() {
				@Override public void itemStateChanged(ItemEvent e) {
					if (e.getStateChange() == ItemEvent.SELECTED) {
						applyComponentStatus();
						if (traits.getSelectedIndex() >= 0) {
							JTextField tt = widthTraits[traits.getSelectedIndex()];
							if (tt.isEditable() && tt.isEditable() && !tt.getText().trim().isEmpty()) {
								tt.requestFocusInWindow();
								tt.selectAll();
							}
						}
					}
				}
			});
			settings.add(traits);

			for (int i = 0; i < widthTraits.length; i++) {
				settings.add(this.widthTraits[i] = this.createSizeText(owner.getResource("splitter.tooltip.optional", "Same as 'Any' with empty value."), focusListener, documentListener));
			}
			for (int i = 0; i < heightTraits.length; i++) {
				settings.add(this.heightTraits[i] = this.createSizeText(owner.getResource("splitter.tooltip.optional", "Same as 'Any' with empty value."), focusListener, documentListener));
			}
		}

		JLabel label38mm, label42mm;
		label38mm = label42mm = null;
		if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
			settings.add(label38mm = new JLabel(IOSAssetCatalogs.SUBTYPE.MM38.toString()));
			settings.add(label42mm = new JLabel(IOSAssetCatalogs.SUBTYPE.MM42.toString()));
			settings.add(width38mm = this.createSizeText(owner.getResource("splitter.tooltip.optional", "Same as 'Any' with empty value."), focusListener, documentListener));
			settings.add(height38mm = this.createSizeText(owner.getResource("splitter.tooltip.optional", "Same as 'Any' with empty value."), focusListener, documentListener));
			settings.add(width42mm = this.createSizeText(owner.getResource("splitter.tooltip.optional", "Same as 'Any' with empty value."), focusListener, documentListener));
			settings.add(height42mm = this.createSizeText(owner.getResource("splitter.tooltip.optional", "Same as 'Any' with empty value."), focusListener, documentListener));
		}

		SpringLayout layout = new SpringLayout();
		layout.putConstraint(SpringLayout.HORIZONTAL_CENTER, labelWidth, 0, SpringLayout.HORIZONTAL_CENTER, widthAny);
		layout.putConstraint(SpringLayout.HORIZONTAL_CENTER, labelHeight, 0, SpringLayout.HORIZONTAL_CENTER, heightAny);
		layout.putConstraint(SpringLayout.NORTH, labelWidth, this.getType() == DEVICE_TYPE.UNIVERSAL ? 8 : 0, SpringLayout.NORTH, settings);
		layout.putConstraint(SpringLayout.NORTH, labelHeight, this.getType() == DEVICE_TYPE.UNIVERSAL ? 8 : 0, SpringLayout.NORTH, settings);

		int gapw = -8;
		int gaph = 8;
		int hgap = 8;
		if (this.getType() == DEVICE_TYPE.DEFAULT || this.getType() == DEVICE_TYPE.UNIVERSAL) {
			this.setBackground(Color.WHITE);
			settings.setBackground(Color.WHITE);
			northPane.setBackground(Color.WHITE);
			this.generateTarget.setBackground(Color.WHITE);
			this.generateRetina4.setBackground(Color.WHITE);
			if (this.getType() == DEVICE_TYPE.UNIVERSAL) {
				this.setBorder(new EmptyBorder(hgap * 2, 8, 0, 8));
			}
		} else {
			hgap = isMacLAF ? 2 : 4;
			this.add(northPane, BorderLayout.NORTH);
			this.setBorder(new EmptyBorder(0, 0, 0, 0));
		}

		if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
			layout.putConstraint(SpringLayout.NORTH, widthAny, hgap, SpringLayout.SOUTH, labelWidth);
			layout.putConstraint(SpringLayout.NORTH, heightAny, hgap, SpringLayout.SOUTH, labelHeight);
		} else {
			JSeparator separator = (new JSeparator(JSeparator.HORIZONTAL));
			//separator.setPreferredSize(new Dimension(4, 4));
			settings.add(separator);

			layout.putConstraint(SpringLayout.NORTH, separator, isMacLAF ? 0 : 2, SpringLayout.SOUTH, labelWidth);
			layout.putConstraint(SpringLayout.WEST, separator, -32, SpringLayout.WEST, widthAny);
			layout.putConstraint(SpringLayout.EAST, separator, 32, SpringLayout.EAST, heightAny);
			layout.putConstraint(SpringLayout.NORTH, widthAny, isMacLAF ? 2 : 10, SpringLayout.SOUTH, separator);
			layout.putConstraint(SpringLayout.NORTH, heightAny, isMacLAF ? 2 : 10, SpringLayout.SOUTH, separator);
		}

		layout.putConstraint(SpringLayout.EAST, widthAny, gapw, SpringLayout.HORIZONTAL_CENTER, settings);
		layout.putConstraint(SpringLayout.WEST, heightAny, gaph, SpringLayout.HORIZONTAL_CENTER, settings);

		if (this.getType() == DEVICE_TYPE.DEFAULT) {
			JLabel labelExtra1 = new JLabel(owner.getResource("splitter.label.extra1", "Syntax: e.g. \"44 px\" or \"100 %\""));
			JLabel labelExtra2 = new JLabel(owner.getResource("splitter.label.extra2", "Pixel(px) = size at @1x"));
			JLabel labelExtra3 = new JLabel(owner.getResource("splitter.label.extra3", "% of original size = size at @3x"));
			labelExtra1.setFont(labelSized.getFont());
			labelExtra1.setForeground(labelSized.getForeground());
			labelExtra2.setFont(labelSized.getFont());
			labelExtra2.setForeground(labelSized.getForeground());
			labelExtra3.setFont(labelSized.getFont());
			labelExtra3.setForeground(labelSized.getForeground());
			labelSized.setText(owner.getResource("splitter.label.extra4", "Also accept empty value either."));
			settings.add(labelExtra1);
			settings.add(labelExtra2);
			settings.add(labelExtra3);

			layout.putConstraint(SpringLayout.HORIZONTAL_CENTER, labelExtra1, 0, SpringLayout.HORIZONTAL_CENTER, settings);
			layout.putConstraint(SpringLayout.NORTH, labelExtra1, hgap * 2, SpringLayout.SOUTH, heightAny);
			layout.putConstraint(SpringLayout.EAST, labelExtra2, 0, SpringLayout.EAST, labelExtra3);
			layout.putConstraint(SpringLayout.NORTH, labelExtra2, hgap, SpringLayout.SOUTH, labelExtra1);
			layout.putConstraint(SpringLayout.HORIZONTAL_CENTER, labelExtra3, 0, SpringLayout.HORIZONTAL_CENTER, settings);
			layout.putConstraint(SpringLayout.NORTH, labelExtra3, 2, SpringLayout.SOUTH, labelExtra2);
			layout.putConstraint(SpringLayout.HORIZONTAL_CENTER, labelSized, 0, SpringLayout.HORIZONTAL_CENTER, settings);
			layout.putConstraint(SpringLayout.NORTH, labelSized, hgap * 15 / 10, SpringLayout.SOUTH, labelExtra3);
		} else {
			layout.putConstraint(SpringLayout.EAST, labelAny, -8, SpringLayout.WEST, widthAny);
			layout.putConstraint(SpringLayout.VERTICAL_CENTER, labelAny, 0, SpringLayout.VERTICAL_CENTER, widthAny);

			if (this.getType() == DEVICE_TYPE.MAC) {
				layout.putConstraint(SpringLayout.HORIZONTAL_CENTER, labelSized, 0, SpringLayout.HORIZONTAL_CENTER, settings);
				layout.putConstraint(SpringLayout.NORTH, labelSized, 16, SpringLayout.SOUTH, widthAny);
			}
			if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
				layout.putConstraint(SpringLayout.EAST, width38mm, gapw, SpringLayout.HORIZONTAL_CENTER, settings);
				layout.putConstraint(SpringLayout.NORTH, width38mm, hgap, SpringLayout.SOUTH, widthAny);
				layout.putConstraint(SpringLayout.WEST, height38mm, gaph, SpringLayout.HORIZONTAL_CENTER, settings);
				layout.putConstraint(SpringLayout.NORTH, height38mm, hgap, SpringLayout.SOUTH, heightAny);

				layout.putConstraint(SpringLayout.EAST, label38mm, -8, SpringLayout.WEST, width38mm);
				layout.putConstraint(SpringLayout.VERTICAL_CENTER, label38mm, 0, SpringLayout.VERTICAL_CENTER, width38mm);

				layout.putConstraint(SpringLayout.EAST, width42mm, gapw, SpringLayout.HORIZONTAL_CENTER, settings);
				layout.putConstraint(SpringLayout.NORTH, width42mm, hgap, SpringLayout.SOUTH, width38mm);
				layout.putConstraint(SpringLayout.WEST, height42mm, gaph, SpringLayout.HORIZONTAL_CENTER, settings);
				layout.putConstraint(SpringLayout.NORTH, height42mm, hgap, SpringLayout.SOUTH, height38mm);

				layout.putConstraint(SpringLayout.EAST, label42mm, -8, SpringLayout.WEST, width42mm);
				layout.putConstraint(SpringLayout.VERTICAL_CENTER, label42mm, 0, SpringLayout.VERTICAL_CENTER, width42mm);

				layout.putConstraint(SpringLayout.HORIZONTAL_CENTER, labelSized, 0, SpringLayout.HORIZONTAL_CENTER, settings);
				layout.putConstraint(SpringLayout.NORTH, labelSized, 6, SpringLayout.SOUTH, width42mm);
			}
			if (this.isSupprtSizeClasses()) {
				for (int i = 0; i < widthTraits.length; i++) {
					layout.putConstraint(SpringLayout.EAST, widthTraits[i], gapw, SpringLayout.HORIZONTAL_CENTER, settings);
					layout.putConstraint(SpringLayout.NORTH, widthTraits[i], hgap, SpringLayout.SOUTH, widthAny);
				}
				for (int i = 0; i < heightTraits.length; i++) {
					layout.putConstraint(SpringLayout.WEST, heightTraits[i], gaph, SpringLayout.HORIZONTAL_CENTER, settings);
					layout.putConstraint(SpringLayout.NORTH, heightTraits[i], hgap, SpringLayout.SOUTH, heightAny);
				}
				layout.putConstraint(SpringLayout.EAST, traits, isMacLAF ? 0 : -8, SpringLayout.WEST, widthTraits[0]);
				layout.putConstraint(SpringLayout.VERTICAL_CENTER, traits, 0, SpringLayout.VERTICAL_CENTER, widthTraits[0]);
				layout.putConstraint(SpringLayout.HORIZONTAL_CENTER, labelSized, 0, SpringLayout.HORIZONTAL_CENTER, settings);
				layout.putConstraint(SpringLayout.NORTH, labelSized, 16, SpringLayout.SOUTH, widthTraits[0]);
			}
		}
		settings.setLayout(layout);

		int preferredWidth = layout.getConstraints(labelSized).getX().getPreferredValue() + layout.getConstraints(labelSized).getWidth().getPreferredValue() + 32;
		if (this.isSupprtSizeClasses()) {
			traits.addItem("HeightClass - Compact");
			preferredWidth = layout.getConstraints(heightAny).getX().getPreferredValue() + layout.getConstraints(heightAny).getWidth().getPreferredValue() + 32;
			if (preferredWidth < layout.getConstraints(labelSized).getWidth().getPreferredValue() + 32) {
				preferredWidth = layout.getConstraints(labelSized).getWidth().getPreferredValue() + 32;
			}
			if (generateRetina4 != null) {
				preferredWidth = layout.getConstraints(traits).getWidth().getPreferredValue()
						+ layout.getConstraints(widthAny).getWidth().getPreferredValue()
						+ layout.getConstraints(heightAny).getWidth().getPreferredValue()
						+ layout.getConstraints(generateRetina4).getWidth().getPreferredValue()
						+ 48;
			}
		}
		int preferredHeight = layout.getConstraints(labelSized).getY().getPreferredValue() + layout.getConstraints(labelSized).getHeight().getPreferredValue() + 8;
		settings.setPreferredSize(new Dimension(preferredWidth, preferredHeight));
		this.add(settings, BorderLayout.CENTER);

		refreshTraits();
		applyComponentStatus();
	}

	/**
	 * Initialize text field to size box.
	 *
	 * @param tooltipText tooltip text
	 * @param focusListener
	 * @param documentListener
	 * @return
	 */
	private JTextField createSizeText(String tooltipText, FocusListener focusListener, DocumentListener documentListener) {
		JTextField textField = new JTextField("", 5);
		Insets insets = new Insets(2, 2, 2, 4);
		textField.setToolTipText(tooltipText);
		textField.setHorizontalAlignment(JTextField.RIGHT);
		textField.setMargin(insets);
		textField.addFocusListener(focusListener);
		textField.getDocument().addDocumentListener(documentListener);
		return textField;
	}

	/**
	 * This device type supports size classes?
	 *
	 * @return true / false
	 */
	private boolean isSupprtSizeClasses() {
		return (	this.getType() == DEVICE_TYPE.UNIVERSAL
				||	this.getType() == DEVICE_TYPE.IPHONE
				||	this.getType() == DEVICE_TYPE.IPAD
				);
	}

	/**
	 * Set size as text to text field.
	 *
	 * @param text
	 * @param value
	 */
	private void setSizeText(JTextField text, String value) {
		try {
			setImageSize(text, value);
		} catch (Throwable t) {
			t.printStackTrace();
			text.requestFocusInWindow();
		}
	}

	/**
	 * Get size text.
	 *
	 * @param text null returns empty string.
	 * @return
	 */
	private String getSizeText(JTextField text) {
		if (text == null) {
			return "";
		}
		return text.getText().trim();
	}

	// Options.
	public void setWidthAny(String s) { this.setSizeText(this.widthAny, s); }
	public String getWidthAny() { return this.getSizeText(this.widthAny); }
	public void setHeightAny(String s) { this.setSizeText(this.heightAny, s); }
	public String getHeightAny() { return  this.getSizeText(this.heightAny); }

	public void setWidthTraits(int index, String s) { this.setSizeText(this.widthTraits[index], s); }
	public String getWidthTraits(int index) { return this.getSizeText(this.widthTraits[index]); }
	public void setHeightTraits(int index, String s) { this.setSizeText(this.heightTraits[index], s); }
	public String getHeightTraits(int index) { return this.getSizeText(this.heightTraits[index]); }

	public void setWidth38mm(String s) { this.setSizeText(this.width38mm, s); }
	public String getWidth38mm() { return this.getSizeText(this.width38mm); }
	public void setHeight38mm(String s) { this.setSizeText(this.height38mm, s); }
	public String getHeight38mm() { return this.getSizeText(this.height38mm); }

	public void setWidth42mm(String s) { this.setSizeText(this.width42mm, s); }
	public String getWidth42mm() { return this.getSizeText(this.width42mm); }
	public void setHeight42mm(String s) { this.setSizeText(this.height42mm, s); }
	public String getHeight42mm() { return this.getSizeText(this.height42mm); }

	public void setGenerateTarget(boolean b) {
		generateTarget.setSelected(b);
		applyComponentStatus();
	}
	public boolean isGenerateTarget() { return this.generateTarget.isSelected(); }
	public void setGenerateRetina4(boolean b) { generateRetina4.setSelected(b); }
	public boolean isGenerateRetina4() { return this.generateRetina4.isSelected(); }

	public void setWidthType(int type) {
		widthType = type;
		refreshTraits();
		applyComponentStatus();
	}
	public int getWidthType() { return widthType; }
	public void setHeightType(int type) {
		heightType = type;
		refreshTraits();
		applyComponentStatus();
	}
	public int getHeightType() { return heightType; }

	/**
	 * Refresh traits combobox text.
	 */
	private void refreshTraits() {
		if (!this.isSupprtSizeClasses()) {
			return;
		}

		int selectedIndex = this.traits.getSelectedIndex();
		final String[] sizes = { "?", "Compact", "Regular" };
		this.traits.removeAllItems();
		this.traits.addItem(String.format("WidthClass - %s", sizes[this.getWidthType()]));
		this.traits.addItem(String.format("HeightClass - %s", sizes[this.getHeightType()]));
		this.traits.addItem(String.format("%s & %s", sizes[this.getWidthType()], sizes[this.getHeightType()]));
		this.traits.setSelectedIndex(selectedIndex);
	}

	/**
	 * Apply components status by item selection.
	 */
	protected void applyComponentStatus() {
		if (this.getType() == DEVICE_TYPE.DEFAULT || this.getType() == DEVICE_TYPE.UNIVERSAL) {
			this.generateRetina4.setEnabled(false);
			// do something some time in the future.
		} else {
			this.generateRetina4.setEnabled(this.generateTarget.isSelected());
			SplitterFrame.setContainerEnabled(settings, this.generateTarget.isSelected());
			if (tabs != null) {
				tabs.setIconAt(tabs.indexOfComponent(SplitterSizePanel.this), generateTarget.isSelected() ? iconChecked : iconUnchecked);
			}
		}
		this.generateRetina4.setVisible(this.getType() == DEVICE_TYPE.IPHONE);

		if (this.getType() == DEVICE_TYPE.DEFAULT || this.getType() == DEVICE_TYPE.MAC) {
			return;
		}

		if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
			width38mm.setEditable(this.generateTarget.isSelected());
			height38mm.setEditable(this.generateTarget.isSelected());
			width42mm.setEditable(this.generateTarget.isSelected());
			height42mm.setEditable(this.generateTarget.isSelected());
			return;
		}

		widthTraits[0].setEditable(this.getWidthType() > 0);
		heightTraits[0].setEditable(this.getWidthType() > 0);
		widthTraits[1].setEditable(this.getHeightType() > 0);
		heightTraits[1].setEditable(this.getHeightType() > 0);
		widthTraits[2].setEditable(this.getWidthType() > 0 && this.getHeightType() > 0);
		heightTraits[2].setEditable(this.getWidthType() > 0 && this.getHeightType() > 0);

		widthTraits[0].setVisible(this.traits.getSelectedIndex() <= 0);
		heightTraits[0].setVisible(this.traits.getSelectedIndex() <= 0);
		widthTraits[1].setVisible(this.traits.getSelectedIndex() == 1);
		heightTraits[1].setVisible(this.traits.getSelectedIndex() == 1);
		widthTraits[2].setVisible(this.traits.getSelectedIndex() == 2);
		heightTraits[2].setVisible(this.traits.getSelectedIndex() == 2);
	}

	/**
	 * Get device type.
	 *
	 * @return device type
	 * @see #DEVICE_TYPE
	 */
	public DEVICE_TYPE getType() {
		return type;
	}

	/**
	 * Apply properties.
	 *
	 * @param props properties
	 * @return same as props
	 */
	protected Properties applyProperties(MainFrame frame, Properties props) {
		Properties def = this.getDefaultProperties(new Properties());
		if (this.getType() == DEVICE_TYPE.DEFAULT || this.getType() == DEVICE_TYPE.UNIVERSAL) {
			// do something some time in the future.
		} else {
			this.setGenerateTarget(frame.getBoolProperty(props, "splitter.generate.target.selected." + this.getType().toString(), def));
			if (this.getType() == DEVICE_TYPE.IPHONE) {
				this.setGenerateRetina4(frame.getBoolProperty(props, "splitter.generate.retina4.selected." + this.getType().toString(), def));
			}
		}

		this.setWidthAny(frame.getStringProperty(props, this.getType() == DEVICE_TYPE.DEFAULT ? "splitter.width1x" : "splitter.width.any." + this.getType().toString(), def));
		this.setHeightAny(frame.getStringProperty(props, this.getType() == DEVICE_TYPE.DEFAULT ? "splitter.height1x" : "splitter.height.any." + this.getType().toString(), def));

		if (this.getType() == DEVICE_TYPE.DEFAULT || this.getType() == DEVICE_TYPE.MAC) {
			return props;
		}

		if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
			this.setWidth38mm(frame.getStringProperty(props, "splitter.width.38mm." + this.getType().toString(), def));
			this.setHeight38mm(frame.getStringProperty(props, "splitter.height.38mm." + this.getType().toString(), def));
			this.setWidth42mm(frame.getStringProperty(props, "splitter.width.42mm." + this.getType().toString(), def));
			this.setHeight42mm(frame.getStringProperty(props, "splitter.height.42mm." + this.getType().toString(), def));
			return props;
		}

		for (int i = 0; i < widthTraits.length; i++) {
			this.setWidthTraits(i, frame.getStringProperty(props, String.format("splitter.width.traits%d.%s", i, this.getType().toString()), def));
		}
		for (int i = 0; i < heightTraits.length; i++) {
			this.setHeightTraits(i, frame.getStringProperty(props, String.format("splitter.height.traits%d.%s", i, this.getType().toString()), def));
		}
		return props;
	}

	/**
	 * Store properties.
	 *
	 * @param props properties
	 * @return same as props
	 */
	protected Properties storeProperties(Properties props) {
		if (this.getType() == DEVICE_TYPE.DEFAULT || this.getType() == DEVICE_TYPE.UNIVERSAL) {
			// do something some time in the future.
		} else {
			props.put("splitter.generate.target.selected." + this.getType().toString(), Boolean.toString(this.isGenerateTarget()));
			if (this.getType() == DEVICE_TYPE.IPHONE) {
				props.put("splitter.generate.retina4.selected." + this.getType().toString(), Boolean.toString(this.isGenerateRetina4()));
			}
		}

		props.put(this.getType() == DEVICE_TYPE.DEFAULT ? "splitter.width1x" : "splitter.width.any." + this.getType().toString(), this.getWidthAny());
		props.put(this.getType() == DEVICE_TYPE.DEFAULT ? "splitter.height1x" : "splitter.height.any." + this.getType().toString(), this.getHeightAny());

		if (this.getType() == DEVICE_TYPE.DEFAULT || this.getType() == DEVICE_TYPE.MAC) {
			return props;
		}

		if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
			props.put("splitter.width.38mm." + this.getType().toString(), this.getWidth38mm());
			props.put("splitter.height.38mm." + this.getType().toString(), this.getHeight38mm());
			props.put("splitter.width.42mm." + this.getType().toString(), this.getWidth42mm());
			props.put("splitter.height.42mm." + this.getType().toString(), this.getHeight42mm());
			return props;
		}

		for (int i = 0; i < widthTraits.length; i++) {
			props.put(String.format("splitter.width.traits%d.%s", i, this.getType().toString()), this.getWidthTraits(i));
		}
		for (int i = 0; i < heightTraits.length; i++) {
			props.put(String.format("splitter.height.traits%d.%s", i, this.getType().toString()), this.getHeightTraits(i));
		}
		return props;
	}

	/**
	 * Get default properties.
	 *
	 * @param props
	 * @return
	 */
	protected Properties getDefaultProperties(Properties props) {
		if (this.getType() == DEVICE_TYPE.DEFAULT || this.getType() == DEVICE_TYPE.UNIVERSAL) {
			// do something some time in the future.
		} else {
			props.put("splitter.generate.target.selected." + this.getType().toString(), Boolean.toString(this.getType() == DEVICE_TYPE.IPHONE || this.getType() == DEVICE_TYPE.IPAD));
			if (this.getType() == DEVICE_TYPE.IPHONE) {
				props.put("splitter.generate.retina4.selected." + this.getType().toString(), Boolean.toString(false));
			}
		}

		String sizeAny = "44 px";
		if (this.getType() == DEVICE_TYPE.IPAD) {
			sizeAny = "66 px";
		} else if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
			sizeAny = "40 px";
		} else if (this.getType() == DEVICE_TYPE.MAC) {
			sizeAny = "64 px";
		}
		props.put(this.getType() == DEVICE_TYPE.DEFAULT ? "splitter.width1x" : "splitter.width.any." + this.getType().toString(), sizeAny);
		props.put(this.getType() == DEVICE_TYPE.DEFAULT ? "splitter.height1x" : "splitter.height.any." + this.getType().toString(), sizeAny);

		if (this.getType() == DEVICE_TYPE.DEFAULT || this.getType() == DEVICE_TYPE.MAC) {
			return props;
		}

		if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
			props.put("splitter.width.38mm." + this.getType().toString(), "35 px");
			props.put("splitter.height.38mm." + this.getType().toString(), "35 px");
			props.put("splitter.width.42mm." + this.getType().toString(), "40 px");
			props.put("splitter.height.42mm." + this.getType().toString(), "40 px");
			return props;
		}

		for (int i = 0; i < widthTraits.length; i++) {
			props.put(String.format("splitter.width.traits%d.%s", i, this.getType().toString()), "");
		}
		for (int i = 0; i < heightTraits.length; i++) {
			props.put(String.format("splitter.height.traits%d.%s", i, this.getType().toString()), "");
		}
		return props;
	}

	/**
	 * Set image size text.
	 * Syntax: e.g. "44 px" or "100 %"
	 *
	 * @param text
	 * @param size
	 */
	private static void setImageSize(JTextField text, String size) {
		if (IOSImageUtil.isNullOrWhiteSpace(size)) {
			text.setText("");
			return;
		}
		size = size.toLowerCase().trim();
		if (size.equals("px") || size.equals("%")) {
			text.setText("");
			return;
		}
		if (size.endsWith("px")) { size = size.replaceAll("px", "").trim(); }
		boolean per = size.endsWith("%");
		if (per) { size = size.replaceAll("%", "").trim(); }
		String newText = new Integer(Math.abs(Integer.parseInt(size))).toString().concat(" ").concat(per ? "%" : "px");
		if (!newText.equals(text.getText())) {
			text.setText(newText);
		}
	}

	/**
	 * Get image size text.
	 *
	 * @param size
	 * @param imageSize
	 * @return
	 */
	protected static double getImageSize(final String inputSize, final double imageSize) {
		if (IOSImageUtil.isNullOrWhiteSpace(inputSize)) {
			return 0;
		}
		String size = inputSize.toLowerCase().trim();
		if (size.equals("px") || size.equals("%")) {
			return 0;
		}

		if (size.endsWith("px")) { size = size.replaceAll("px", "").trim(); }
		boolean per = size.endsWith("%");
		if (per) { size = size.replaceAll("%", "").trim(); }
		return per ? (imageSize * (Double.parseDouble(size) / 100.0) / 3.0) : Double.parseDouble(size);
	}

	/**
	 * Add image queue.
	 *
	 * @param owner
	 * @param queue
	 * @param srcFile original image file path
	 * @param outputDir output directory path
	 * @return
	 * @throws Exception
	 */
	protected LinkedList<IOSImageSet> addImageQueue(final MainFrame owner, LinkedList<IOSImageSet> queue, final File srcFile, final File outputDir, final String suffix) throws Exception {
		if (this.getWidthAny().isEmpty() && this.getHeightAny().isEmpty()) {
			throw new Exception(owner.getResource("splitter.error.empty.both", "Both width and height are empty. Required at least either."));
		}

		if (this.getType() == DEVICE_TYPE.DEFAULT) {
			queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x1, null, null, null, null, suffix));
			queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x2, null, null, null, null, suffix));
			queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x3, null, null, null, null, suffix));
			return queue;
		}

		if (this.getType() != DEVICE_TYPE.UNIVERSAL && !this.isGenerateTarget()) {
			// Not output target.
			return queue;
		}

		if (this.getType() == DEVICE_TYPE.APPLE_WATCH) {
			IOSImageSet.IDIOM idiom = IOSImageSet.IDIOM.APPLEWATCH;
			queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x2, idiom, null, null, null, suffix));
			queue.add(createImageSet(outputDir, srcFile, this.getWidth38mm(), this.getWidthAny(), this.getHeight38mm(), this.getHeightAny(), IOSImageSet.SCALE.x2, idiom, null, null, IOSImageSet.SUBTYPE.MM38, suffix).setSubtype(null).setOption(IOSImageSet.JSON_KEY.SCREEN_WIDTH, IOSImageSet.JSON_VALUE.SCREEN_WIDTH_38MM));
			queue.add(createImageSet(outputDir, srcFile, this.getWidth42mm(), this.getWidthAny(), this.getHeight42mm(), this.getHeightAny(), IOSImageSet.SCALE.x2, idiom, null, null, IOSImageSet.SUBTYPE.MM42, suffix).setSubtype(null).setOption(IOSImageSet.JSON_KEY.SCREEN_WIDTH, IOSImageSet.JSON_VALUE.SCREEN_WIDTH_42MM));
			return queue;
		}

		if (this.getType() == DEVICE_TYPE.MAC) {
			queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x1, IOSAssetCatalogs.IDIOM.MAC, null, null, null, suffix));
			queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x2, IOSAssetCatalogs.IDIOM.MAC, null, null, null, suffix));
			return queue;
		}

		IOSImageSet.IDIOM idiom = IOSImageSet.IDIOM.UNIVERSAL;
		if (this.getType() == DEVICE_TYPE.IPHONE) {
			idiom = IOSAssetCatalogs.IDIOM.IPHONE;
		} else if (this.getType() == DEVICE_TYPE.IPAD) {
			idiom = IOSAssetCatalogs.IDIOM.IPAD;
		}

		queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x1, idiom, null, null, null, suffix));
		queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x2, idiom, null, null, null, suffix));
		if (this.getType() == DEVICE_TYPE.IPHONE && this.isGenerateRetina4())
			{ queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x2, idiom, null, null, IOSImageSet.SUBTYPE.RETINA4, suffix)); }
		if (this.getType() == DEVICE_TYPE.UNIVERSAL || this.getType() == DEVICE_TYPE.IPHONE)
			{ queue.add(createImageSet(outputDir, srcFile, this.getWidthAny(), null, this.getHeightAny(), null, IOSImageSet.SCALE.x3, idiom, null, null, null, suffix)); }

		final JSON_VALUE[] sizeClasses = { IOSImageSet.JSON_VALUE.SIZE_CLASS_ANY, IOSImageSet.JSON_VALUE.SIZE_CLASS_COMPACT, IOSImageSet.JSON_VALUE.SIZE_CLASS_REGULAR };
		JSON_VALUE widthClass = sizeClasses[this.getWidthType()];
		JSON_VALUE heightClass = sizeClasses[this.getHeightType()];

		if (widthClass != IOSImageSet.JSON_VALUE.SIZE_CLASS_ANY) {
			queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(0), this.getWidthAny(), this.getHeightTraits(0), this.getHeightAny(), IOSImageSet.SCALE.x1, idiom, widthClass, null, null, suffix));
			queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(0), this.getWidthAny(), this.getHeightTraits(0), this.getHeightAny(), IOSImageSet.SCALE.x2, idiom, widthClass, null, null, suffix));
			if (this.getType() == DEVICE_TYPE.IPHONE && this.isGenerateRetina4())
				{ queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(0), this.getWidthAny(), this.getHeightTraits(0), this.getHeightAny(), IOSImageSet.SCALE.x2, idiom, widthClass, null, IOSImageSet.SUBTYPE.RETINA4, suffix)); }
			if (this.getType() == DEVICE_TYPE.UNIVERSAL || this.getType() == DEVICE_TYPE.IPHONE)
				{ queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(0), this.getWidthAny(), this.getHeightTraits(0), this.getHeightAny(), IOSImageSet.SCALE.x3, idiom, widthClass, null, null, suffix)); }
		}

		if (heightClass != IOSImageSet.JSON_VALUE.SIZE_CLASS_ANY) {
			queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(1), this.getWidthAny(), this.getHeightTraits(1), this.getHeightAny(), IOSImageSet.SCALE.x1, idiom, null, heightClass, null, suffix));
			queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(1), this.getWidthAny(), this.getHeightTraits(1), this.getHeightAny(), IOSImageSet.SCALE.x2, idiom, null, heightClass, null, suffix));
			if (this.getType() == DEVICE_TYPE.IPHONE && this.isGenerateRetina4())
				{ queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(1), this.getWidthAny(), this.getHeightTraits(1), this.getHeightAny(), IOSImageSet.SCALE.x2, idiom, null, heightClass, IOSImageSet.SUBTYPE.RETINA4, suffix)); }
			if (this.getType() == DEVICE_TYPE.UNIVERSAL || this.getType() == DEVICE_TYPE.IPHONE)
				{ queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(1), this.getWidthAny(), this.getHeightTraits(1), this.getHeightAny(), IOSImageSet.SCALE.x3, idiom, null, heightClass, null, suffix)); }
		}

		if (widthClass != IOSImageSet.JSON_VALUE.SIZE_CLASS_ANY && heightClass != IOSImageSet.JSON_VALUE.SIZE_CLASS_ANY) {
			queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(2), this.getWidthAny(), this.getHeightTraits(2), this.getHeightAny(), IOSImageSet.SCALE.x1, idiom, widthClass, heightClass, null, suffix));
			queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(2), this.getWidthAny(), this.getHeightTraits(2), this.getHeightAny(), IOSImageSet.SCALE.x2, idiom, widthClass, heightClass, null, suffix));
			if (this.getType() == DEVICE_TYPE.IPHONE && this.isGenerateRetina4())
				{ queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(2), this.getWidthAny(), this.getHeightTraits(2), this.getHeightAny(), IOSImageSet.SCALE.x2, idiom, widthClass, heightClass, IOSImageSet.SUBTYPE.RETINA4, suffix)); }
			if (this.getType() == DEVICE_TYPE.UNIVERSAL || this.getType() == DEVICE_TYPE.IPHONE)
				{ queue.add(createImageSet(outputDir, srcFile, this.getWidthTraits(2), this.getWidthAny(), this.getHeightTraits(2), this.getHeightAny(), IOSImageSet.SCALE.x3, idiom, widthClass, heightClass, null, suffix)); }
		}
		return queue;
	}


	/**
	 * Create IOImageSet.
	 *
	 * @param outputDir	output directory path
	 * @param srcFile	original file path
	 * @param width		output image width
	 * @param height	output image height
	 * @param scale		output image scale
	 * @param idiom		output image idiom
	 * @param widthClass	output image width class
	 * @param heightClass	output image height class
	 * @param subtype	output image subtype
	 * @return
	 */
	private IOSImageSet createImageSet(final File outputDir, final File srcFile, final String width, final String defaultWidth, final String height, final String defaultHeight, final IOSImageSet.SCALE scale, final IOSImageSet.IDIOM idiom, final IOSImageSet.JSON_VALUE widthClass, final IOSImageSet.JSON_VALUE heightClass, final IOSImageSet.SUBTYPE subtype, final String suffix) {
		String imageName = srcFile.getName();
		int idx = imageName.lastIndexOf(".");
		if (idx >= 0) {
			imageName = imageName.substring(0, idx);
		}
		File dstDir = outputDir;
		if (this.getType() != DEVICE_TYPE.DEFAULT) {
			dstDir = new File(outputDir, String.format("%s.imageset", imageName));
		}
		String imageFilename = imageName;
		imageFilename = imageFilename.concat(idiom == null ? "" : "-" + idiom);
		imageFilename = imageFilename.concat(widthClass == null ? "" : "-" + widthClass + "w");
		imageFilename = imageFilename.concat(heightClass == null ? "" : "-" + heightClass + "h");
		imageFilename = imageFilename.concat(subtype == null ? "" : "-" + subtype);
		File dstFile = new File(dstDir, String.format("%s@%s.%s", imageFilename, scale, suffix));

		String outputWidth;
		String outputHeight;
		if (IOSImageUtil.isNullOrWhiteSpace(width) && IOSImageUtil.isNullOrWhiteSpace(height)) {
			// Apply 'Any' value when both empty.
			outputWidth = defaultWidth == null ? "" : defaultWidth;
			outputHeight = defaultHeight == null ? "" : defaultHeight;
		} else {
			outputWidth = width;
			outputHeight = height;
		}

		return (new IOSImageSet()).setWidthAsString(outputWidth)
									.setHeightAsString(outputHeight)
									.setScale(scale)
									.setIdiom(idiom)
									.setOriginalFile(srcFile)
									.setFile(dstFile)
									.setImageName(imageName)
									.setSubtype(subtype)
									.setSuffix(suffix)
									.setOption(IOSImageSet.JSON_KEY.WIDTH_CLASS, widthClass)
									.setOption(IOSImageSet.JSON_KEY.HEIGHT_CLASS, heightClass)
									;
	}

	/**
	 * Create SplitterSizePanel as new tab.
	 *
	 * @param type	device type
	 * @param tabs	tabbed pane
	 * @param owner
	 * @return
	 */
	protected static SplitterSizePanel newAsTab(final SplitterSizePanel.DEVICE_TYPE type, final JTabbedPane tabs, final MainFrame owner) {
		SplitterSizePanel ssp = new SplitterSizePanel(type, owner);
		tabs.addTab(ssp.getType().toString(), ssp.iconUnchecked, ssp);
		ssp.tabs = tabs;
		ssp.setBackground(tabs.getBackground());
		tabs.setBackgroundAt(tabs.getTabCount() - 1, new Color(0xEEEEEE));
		return ssp;
	}

	/**
	 * Create size changed item listner.
	 *
	 * @param owner
	 * @param widthType
	 * @param heightType
	 * @return
	 */
	protected ItemListener createSizeChangedItemListener(final MainFrame owner, final JComboBox widthType, final JComboBox heightType) {
		return (new ItemListener() {
			@Override public void itemStateChanged(ItemEvent e) {
				owner.setStorePropertiesRequested(true);
				setWidthType(widthType.getSelectedIndex());
				setHeightType(heightType.getSelectedIndex());
				refreshTraits();
				applyComponentStatus();
			}
		});
	}
}