package charactermanaj.ui;

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TooManyListenersException;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JColorChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;

import charactermanaj.Main;
import charactermanaj.clipboardSupport.ClipboardUtil;
import charactermanaj.graphics.AsyncImageBuilder;
import charactermanaj.graphics.ColorConvertedImageCachedLoader;
import charactermanaj.graphics.ImageBuildJobAbstractAdaptor;
import charactermanaj.graphics.ImageBuilder.ImageOutput;
import charactermanaj.graphics.io.ImageSaveHelper;
import charactermanaj.model.AppConfig;
import charactermanaj.model.CharacterData;
import charactermanaj.model.ColorGroup;
import charactermanaj.model.PartsCategory;
import charactermanaj.model.PartsColorInfo;
import charactermanaj.model.PartsColorManager;
import charactermanaj.model.PartsIdentifier;
import charactermanaj.model.PartsSet;
import charactermanaj.model.WorkingSet;
import charactermanaj.model.io.CharacterDataPersistent;
import charactermanaj.model.io.PartsImageDirectoryWatchAgent;
import charactermanaj.model.io.PartsImageDirectoryWatchEvent;
import charactermanaj.model.io.PartsImageDirectoryWatchListener;
import charactermanaj.model.io.RecentDataPersistent;
import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelEvent;
import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelListener;
import charactermanaj.ui.PreviewPanel.PreviewPanelEvent;
import charactermanaj.ui.PreviewPanel.PreviewPanelListener;
import charactermanaj.ui.model.ColorChangeEvent;
import charactermanaj.ui.model.ColorChangeListener;
import charactermanaj.ui.model.ColorGroupCoordinator;
import charactermanaj.ui.model.PartsColorCoordinator;
import charactermanaj.ui.model.PartsSelectionManager;
import charactermanaj.util.DesktopUtilities;
import charactermanaj.util.ErrorMessageHelper;
import charactermanaj.util.SystemUtil;
import charactermanaj.util.LocalizedResourcePropertyLoader;
import charactermanaj.util.UIHelper;
import charactermanaj.util.UserData;
import charactermanaj.util.UserDataFactory;


/**
 * メインフレーム.<br>
 * アプリケーションがアクティブである場合は最低でも1つのメインフレームが表示されている.<br>
 * @author seraphy
 */
public class MainFrame extends JFrame {

	private static final long serialVersionUID = 1L;
	
	private static final Logger logger = Logger.getLogger(MainFrame.class.getName());

	
	protected static final String STRINGS_RESOURCE = "strings/mainframe";
	
	protected static final String MENU_STRINGS_RESOURCE = "menu/menu";
	

	/**
	 * メインフレームのアイコン.<br>
	 */
	protected BufferedImage icon;
	

	/**
	 * 現在アクティブなメインフレーム.<br>
	 * フォーカスが切り替わるたびにアクティブフレームを追跡する.<br>
	 * Mac OS XのAbout/Preferences/Quitのシステムメニューからよびだされた場合に
	 * オーナーたるべきメインフレームを識別するためのもの.<br>
	 */
	private static volatile MainFrame activedMainFrame;


	/**
	 * このメインフレームが対象とするキャラクターデータ.<br>
	 */
	protected CharacterData characterData;

	
	/**
	 * プレビューペイン
	 */
	private PreviewPanel previewPane;
	
	/**
	 * パーツ選択マネージャ
	 */
	protected PartsSelectionManager partsSelectionManager;
	
	/**
	 * パーツ選択パネルリスト
	 */
	protected ImageSelectPanelList imageSelectPanels;
	
	/**
	 * カラーグループのマネージャ
	 */
	protected ColorGroupCoordinator colorGroupCoordinator;
	
	/**
	 * パーツカラーのマネージャ
	 */
	protected PartsColorCoordinator partsColorCoordinator;
	

	/**
	 * キャッシュつきのイメージローダ.<br>
	 */
	private ColorConvertedImageCachedLoader imageLoader;

	/**
	 * パーツを組み立てて1つのプレビュー可能なイメージを構築するためのビルダ
	 */
	private AsyncImageBuilder imageBuilder;

	
	/**
	 * パーツイメージを画像として保存する場合のヘルパー.<br>
	 * 最後に使ったディレクトリを保持するためのメンバ変数としている.<br>
	 */
	private ImageSaveHelper imageSaveHelper = new ImageSaveHelper();
	

	/**
	 * パーツディレクトリを定期的にチェックし、パーツイメージが変更・追加・削除されている場合に
	 * パーツリストを更新するためのウォッチャー 
	 */
	private PartsImageDirectoryWatchAgent watchAgent;
	
	/**
	 * デフォルトのパーツセット表示名
	 */
	private String defaultPartsSetTitle;
	
	/**
	 * 最後に使用したプリセット.<br>
	 * (一度もプリセットを使用していなければnull).
	 */
	private PartsSet lastUsePresetParts;
	
	
	/**
	 * すべてのメインフレームで使用中のキャラクターデータのコレクション.<br>
	 */
	private static final HashMap<String, Integer> activeCharacterDatas = new HashMap<String, Integer>(); 
	
	/**
	 * アクティブなメインフレームを設定する.
	 * @param mainFrame メインフレーム
	 */
	public static void setActivedMainFrame(MainFrame mainFrame) {
		if (mainFrame == null) {
			throw new IllegalArgumentException();
		}
		activedMainFrame = mainFrame;
	}
	
	/**
	 * 現在アクティブなメインフレームを取得する.
	 * @return メインフレーム
	 */
	public static MainFrame getActivedMainFrame() {
		return activedMainFrame;
	}
	
	
	/**
	 * キャラクターデータが使用中であるか?<br>
	 * キャラクターデータのDocBaseをもとに判断する.<br>
	 * nullを指定した場合は常にfalseを返す.<br>
	 * @param characterData キャラクターデータ、またはnull
	 * @return 使用中であればtrue
	 */
	public static boolean isUsingCharacterData(CharacterData characterData) {
		if (characterData == null) {
			return false;
		}
		URL characterDocBase = characterData.getDocBase();
		if (characterDocBase == null) {
			return false;
		}
		synchronized (activeCharacterDatas) {
			Integer cnt = activeCharacterDatas.get(characterDocBase.toString());
			if (cnt != null && cnt.intValue() > 0) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * キャラクターデータが変更されたことを通知される.
	 * @param cd キャラクターデータ(変更前)
	 * @param newCd キャラクターデータ(変更後)
	 * @param structureCompatible 構造が同一であるか?
	 */
	public static void notifyChangeCharacterData(CharacterData cd, CharacterData newCd) {
		if (cd == null || newCd == null) {
			throw new IllegalArgumentException();
		}
		if (!newCd.isValid()) {
			return;
		}
		
		// Frameのうち、ネイティブリソースと関連づけられているアクティブなフレームを調査
		for (Frame frame : JFrame.getFrames()) {
			if (frame.isDisplayable() && frame instanceof MainFrame) {
				MainFrame mainFrame = (MainFrame) frame;
				if (cd.getDocBase().equals(mainFrame.characterData.getDocBase())) {
					mainFrame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
					try {
						// 現在情報の保存
						mainFrame.saveWorkingSet();

						// 画面構成の再構築
						mainFrame.initComponent(newCd);

					} finally {
						mainFrame.setCursor(Cursor.getDefaultCursor());
					}
				}
			}
		}
	}

	/**
	 * お気に入りデータが変更されたことを通知される.
	 * @param cd キャラクターデータ
	 */
	public static void notifyChangeFavorites(CharacterData cd) {
		if (cd == null) {
			throw new IllegalArgumentException();
		}
		
		// Frameのうち、ネイティブリソースと関連づけられているアクティブなフレームを調査
		for (Frame frame : JFrame.getFrames()) {
			if (frame.isDisplayable() && frame instanceof MainFrame) {
				MainFrame mainFrame = (MainFrame) frame;
				if (cd.getDocBase().equals(mainFrame.characterData.getDocBase())) {
					mainFrame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
					try {
						// お気に入りを最新の状態に読み直し
						mainFrame.refreshFavorites();

					} finally {
						mainFrame.setCursor(Cursor.getDefaultCursor());
					}
				}
			}
		}
	}

	/**
	 * キャラクターデータが使用中であることを登録する。
	 * @param characterData キャラクターデータ
	 */
	protected static void registerUsedCharacterData(CharacterData characterData) {
		if (characterData == null) {
			return;
		}
		synchronized (activeCharacterDatas) {
			URL characterDocBase = characterData.getDocBase();
			if (characterDocBase != null) {
				Integer cnt = activeCharacterDatas.get(characterDocBase.toString());
				if (cnt == null) {
					cnt = Integer.valueOf(0);
				}
				cnt = Integer.valueOf(cnt.intValue() + 1);
				activeCharacterDatas.put(characterDocBase.toString(), cnt);
			}
		}
	}
	
	/**
	 * キャラクターデータの使用中であることを登録解除する。
	 * @param characterData キャラクターデータ
	 */
	protected static void unregisterUsedCharacterData(CharacterData characterData) {
		if (characterData == null) {
			return;
		}
		// 使用中のキャラクターデータとしてのカウントを減らす
		synchronized (activeCharacterDatas) {
			URL characterDocBase = characterData.getDocBase();
			if (characterDocBase != null) {
				int cnt = activeCharacterDatas.get(characterDocBase.toString());
				cnt = cnt - 1;
				activeCharacterDatas.put(characterDocBase.toString(), Integer.valueOf(cnt));
			}
		}
	}
	
	/**
	 * メインフレームを構築する.
	 * @param characterData キャラクターデータ
	 */
	public MainFrame(CharacterData characterData) {
		if (characterData == null) {
			throw new IllegalArgumentException();
		}
		
		setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				onCloseProfile();
			}
			@Override
			public void windowClosed(WindowEvent e) {
				imageBuilder.stop();
				watchAgent.stop();
			}
			@Override
			public void windowActivated(WindowEvent e) {
				setActivedMainFrame(MainFrame.this);
			}
			@Override
			public void windowOpened(WindowEvent e) {
				// do nothing.
			}
		});

		icon = UIHelper.getInstance().getImage("icons/icon.png");
		setIconImage(icon);
		
		// 画面コンポーネント作成
		initComponent(characterData);
		JMenuBar menuBar = createMenuBar();
		setJMenuBar(menuBar);

		Dimension imageSize = characterData.getImageSize();
		// 画像サイズ300x400を基準サイズとして、それ以下にはならない.
		// アプリケーション設定の最大サイズ以上の場合はウィンドウサイズは固定してスクロールバーに任せる
		AppConfig appConfig = AppConfig.getInstance();
		int maxWidth = appConfig.getMainFrameMaxWidth();
		int maxHeight = appConfig.getMainFrameMaxHeight();
		int imageWidth = min(maxWidth, max(300, imageSize != null ? imageSize.width : 0));
		int imageHeight = min(maxHeight, max(400, imageSize != null ? imageSize.height : 0));
		// 300x400の画像の場合にメインフレームが600x550だとちょうどいい感じ.
		// それ以上大きい画像の場合は増えた分だけフレームを大きくしておく.
		setSize(imageWidth - 300 + 600, imageHeight - 400 + 550);

		//setLocationRelativeTo(null);
		setLocationByPlatform(true);
	}
	
	/**
	 * 画面コンポーネントを設定します.<br>
	 * すでに設定されている場合は一旦削除されたのちに再作成されます.<br>
	 */
	private void initComponent(CharacterData characterData) {
		
		CharacterData oldCd;
		synchronized (this) {
			oldCd = this.characterData;
			if (oldCd != null) {
				// 使用中のキャラクターデータであることを登録解除する。
				unregisterUsedCharacterData(oldCd);
			}
			this.characterData = characterData;

			// 使用中のキャラクターデータであることを登録する.
			registerUsedCharacterData(characterData);
		}
		
		// 設定まわり準備
		AppConfig appConfig = AppConfig.getInstance();
		Properties strings = LocalizedResourcePropertyLoader.getInstance()
				.getLocalizedProperties(STRINGS_RESOURCE);

		// タイトル表示
		String title;
		if (Main.isMacOSX()) {
			// Mac OS Xの場合はウィンドウにタイトルはつけない。
			title = "";
		} else {
			title = strings.getProperty("title");
		}
		setTitle(title + characterData.getName());
		
		// デフォルトのパーツセット表示名
		defaultPartsSetTitle = strings.getProperty("defaultPartsSetTitle");
		
		// エージェントを停止
		if (watchAgent != null) {
			watchAgent.stop();
		}
		// イメージビルダを停止
		if (imageBuilder != null) {
			imageBuilder.stop();
		}

		// コンポーネント配置
		Container contentPane = getContentPane();
		
		// すでにあるコンポーネントを削除
		for (Component comp : contentPane.getComponents()) {
			contentPane.remove(comp);
		}
		// 開いている検索ダイアログを閉じる
		closeAllSearchDialog();

		PartsColorManager partsColorManager = characterData.getPartsColorManager();

		imageLoader = new ColorConvertedImageCachedLoader();
		imageBuilder = new AsyncImageBuilder(imageLoader);
		partsSelectionManager = new PartsSelectionManager(partsColorManager);
		colorGroupCoordinator = new ColorGroupCoordinator(partsSelectionManager, partsColorManager);
		partsColorCoordinator = new PartsColorCoordinator(characterData, partsColorManager, colorGroupCoordinator);
		watchAgent = new PartsImageDirectoryWatchAgent(characterData);

		previewPane = new PreviewPanel();
		previewPane.setTitle(defaultPartsSetTitle);
		previewPane.addPreviewPanelListener(new PreviewPanelListener() {
			public void addFavorite(PreviewPanelEvent e) {
				onRegisterFavorite();
			}
			public void changeBackgroundColor(PreviewPanelEvent e) {
				onChangeBgColor();
			}
			public void copyPicture(PreviewPanelEvent e) {
				onCopy();
			}
			public void savePicture(PreviewPanelEvent e) {
				onSavePicture();
			}
			public void showInformation(PreviewPanelEvent e) {
				onInformation();
			}
			public void flipHorizontal(PreviewPanelEvent e) {
				onFlipHolizontal();
			}
		});
		
		imageSelectPanels = new ImageSelectPanelList();
		
		JPanel imgSelectPanelsPanel = new JPanel();
		BoxLayout bl = new BoxLayout(imgSelectPanelsPanel, BoxLayout.PAGE_AXIS);
		imgSelectPanelsPanel.setLayout(bl);
		for (PartsCategory category : characterData.getPartsCategories()) {
			final ImageSelectPanel imageSelectPanel = new ImageSelectPanel(category, characterData);
			imgSelectPanelsPanel.add(imageSelectPanel);
			imageSelectPanels.add(imageSelectPanel);
			partsSelectionManager.register(imageSelectPanel);
		}
		
		JScrollPane imgSelectPanelsPanelSp = new JScrollPane(imgSelectPanelsPanel) {
			private static final long serialVersionUID = 1L;
			@Override
			public JScrollBar createVerticalScrollBar() {
				JScrollBar sb = super.createVerticalScrollBar();
				sb.setUnitIncrement(12);
				return sb;
			}
		};
		imgSelectPanelsPanelSp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

		JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, imgSelectPanelsPanelSp, previewPane);
		contentPane.add(splitPane, BorderLayout.CENTER);

		
		imgSelectPanelsPanelSp.requestFocus();
		
		ArrayList<ColorGroup> colorGroups = new ArrayList<ColorGroup>();
		colorGroups.addAll(characterData.getColorGroups());
		
		final ColorChangeListener colorChangeListener = new ColorChangeListener() {
			public void onColorGroupChange(ColorChangeEvent event) {
				// do nothing.
			}
			public void onColorChange(ColorChangeEvent event) {
				MainFrame.this.requestPreview();
			}
		};
		colorGroupCoordinator.addColorChangeListener(colorChangeListener);
		
		for (int idx = 0; idx < imageSelectPanels.size(); idx++) {
			ImageSelectPanel imageSelectPanel = imageSelectPanels.get(idx);
			final PartsCategory partsCategory = imageSelectPanel.getPartsCategory();
			final ColorDialog colorDialog = new ColorDialog(this, partsCategory, colorGroups);
			colorGroupCoordinator.registerColorDialog(colorDialog);
			partsColorCoordinator.register(imageSelectPanel, colorDialog);
			final int curidx = idx;
			imageSelectPanel.addImageSelectListener(new ImageSelectPanelListener() {
				public void onChangeColor(ImageSelectPanelEvent event) {
					colorDialog.adjustLocation(curidx);
					colorDialog.setVisible(!colorDialog.isVisible());
				}
				public void onPreferences(ImageSelectPanelEvent event) {
					// do nothing. (not supported)
				}
				public void onChange(ImageSelectPanelEvent event) {
					MainFrame.this.requestPreview();
				}
				public void onSelectChange(ImageSelectPanelEvent event) {
					// do nothing.
				}
			});
			imageSelectPanel.addAncestorListener(new AncestorListener() {
				public void ancestorAdded(AncestorEvent event) {
				}
				public void ancestorMoved(AncestorEvent event) {
				}
				public void ancestorRemoved(AncestorEvent event) {
					// パネルもしくは、その親が削除されたときにダイアログも非表示とする。
					colorDialog.setVisible(false);
				}
			});
		}
		
		partsSelectionManager.loadParts();
		
		if (!loadWorkingSet()) {
			String defaultPresetId = characterData.getDefaultPartsSetId();
			PartsSet presetParts = null;
			if (defaultPresetId != null) {
				presetParts = characterData.getPartsSets().get(defaultPresetId);
			}
			if (presetParts == null) {
				partsColorCoordinator.initColorDialog();
				requestPreview();
			} else {
				selectPresetParts(presetParts);
			}
		}

		// 選択されているパーツを見える状態にする
		scrollToSelectedParts();

		// 非同期イメージローダの処理開始
		if (!imageBuilder.isAlive()) {
			imageBuilder.start();
		}
		
		// ドロップターゲットの設定
		DropTarget dt = new DropTarget();
		try {
			dt.addDropTargetListener(new DropTargetAdapter() {
				public void drop(DropTargetDropEvent dtde) {
					onDrop(dtde);
				}
			});
		} catch (TooManyListenersException ex) {
			throw new RuntimeException(ex.getMessage(), ex);
		}
		imgSelectPanelsPanelSp.setDropTarget(dt);
		
		// ディレクトリを監視し変更があった場合にパーツをリロードするリスナ
		watchAgent.addPartsImageDirectoryWatchListener(new PartsImageDirectoryWatchListener() {
			public void detectPartsImageChange(PartsImageDirectoryWatchEvent e) {
				Runnable refreshJob = new Runnable() {
					public void run() {
						onDetectPartsImageChange();
					}
				};
				if (SwingUtilities.isEventDispatchThread()) {
					refreshJob.run();
				} else {
					SwingUtilities.invokeLater(refreshJob);
				}
			}
		});

		// 監視が有効であれば、ディレクトリの監視をスタートする
		if (appConfig.isEnableDirWatch() && characterData.isWatchDirectory()) {
			watchAgent.start();
		}
		
		// コンポーネントの再構築の場合
		if (oldCd != null) {
			validate();
		}
	}

	/**
	 * パーツが変更されたことを検知した場合.<br>
	 * パーツデータをリロードし、各カテゴリのパーツ一覧を再表示させ、プレビューを更新する.<br>
	 */
	protected void onDetectPartsImageChange() {
		if (characterData.reloadPartsData()) {
			partsSelectionManager.loadParts();
			requestPreview();
		}
	}

	/**
	 * すべてのカテゴリのリストで選択中のアイテムが見えるようにスクロールする.
	 */
	protected void scrollToSelectedParts() {
		partsSelectionManager.scrollToSelectedParts();
	}

	/**
	 * プリセットを適用しキャラクターイメージを再構築します.
	 * @param presetParts
	 */
	protected void selectPresetParts(PartsSet presetParts) {
		// 最後に使用したプリセットとして記憶する.
		lastUsePresetParts = presetParts;
		// プリセットパーツで選択を変える
		partsSelectionManager.selectPartsSet(presetParts);
		// カラーパネルを選択されているアイテムをもとに再設定する
		partsColorCoordinator.initColorDialog();
		// 再表示
		requestPreview();
	}
	
	/**
	 * お気に入りメニューが開いたとき
	 * @param menu
	 */
	protected void onSelectedFavoriteMenu(JMenu menu) {
		int mx = menu.getMenuComponentCount();
		int separatorIdx = -1;
		for (int idx = 0; idx < mx; idx++) {
			Component item = menu.getMenuComponent(idx);
			if (item instanceof JSeparator) {
				separatorIdx = idx;
				break;
			}
		}
		// 既存メニューの削除
		if (separatorIdx > 0) {
			while (menu.getMenuComponentCount() > separatorIdx + 1) {
				menu.remove(separatorIdx + 1);
			}
		}
		
		// 表示順にソート
		ArrayList<PartsSet> partssets = new ArrayList<PartsSet>();
		partssets.addAll(characterData.getPartsSets().values());
		Collections.sort(partssets, new Comparator<PartsSet>() {
			public int compare(PartsSet o1, PartsSet o2) {
				int ret = o1.getLocalizedName().compareTo(o2.getLocalizedName());
				if (ret == 0) {
					ret = o1.getPartsSetId().compareTo(o2.getPartsSetId());
				}
				if (ret == 0) {
					ret = o1.hashCode() - o2.hashCode();
				}
				return ret;
			}
		});
		
		// JRE1.5ではアンチエイリアスをする.
		final boolean needAntiAlias = isNeedAntialias();

		// メニューの再構築

		for (final PartsSet presetParts : partssets) {
			JMenuItem favoriteMenu = new JMenuItem() {
				private static final long serialVersionUID = 1L;

				@Override
				public void paint(Graphics g) {
					if (needAntiAlias) {
						((Graphics2D) g).setRenderingHint(
								RenderingHints.KEY_TEXT_ANTIALIASING,
								RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
					}
					super.paint(g);
				}
			};
			favoriteMenu.setName(presetParts.getPartsSetId());
			favoriteMenu.setText(presetParts.getLocalizedName());
			if (presetParts.isPresetParts()) {
				Font font = favoriteMenu.getFont();
				favoriteMenu.setFont(font.deriveFont(Font.BOLD));
			}
			favoriteMenu.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					selectPresetParts(presetParts);
				}
			});
			menu.add(favoriteMenu);
		}
	}
	
	/**
	 * 最後に選択されたお気に入りと同じ構成であれば、
	 * このお気に入りの名前をプレビューペインのタイトルに設定する.<br>
	 * そうでなければデフォルトのパーツセット名(no titleとか)を表示する.<br>
	 * 色情報が異なる場合に末尾に「*」マークがつけられる.<br>
	 * @param requestPartsSet 表示するパーツセット(名前は設定されていなくて良い。お気に入り側を使うので。), nullの場合はデフォルトのパーツ名
	 */
	protected void showPresetName(PartsSet requestPartsSet) {
		previewPane.setTitle(getSuggestPartsSetName(requestPartsSet, true, defaultPartsSetTitle));
	}
	
	/**
	 * パーツセット名を推定する.<br>
	 * 最後に選択されたお気に入りと同じ構成であれば、
	 * このお気に入りの名前をプレビューペインのタイトルに設定する.<br>
	 * そうでなければデフォルトのパーツセット名(no titleとか)を表示する.<br>
	 * @param requestPartsSet 表示するパーツセット(名前は設定されていなくて良い。お気に入り側を使うので。),nullの場合はデフォルト名を返す
	 * @param markColorChange 色情報が異なる場合に末尾に「*」マークをつける場合はtrue
	 * @param defaultPartsSetName 該当しない場合に使用されるデフォルトパーツセット名
	 */
	private String getSuggestPartsSetName(PartsSet requestPartsSet, boolean markColorChange, String defaultPartsSetName) {
		String partsSetTitle = null;
		if (lastUsePresetParts != null &&
				PartsSet.isSameStructure(requestPartsSet, lastUsePresetParts)) {
			partsSetTitle = lastUsePresetParts.getLocalizedName();
			if (markColorChange && !PartsSet.isSameColor(requestPartsSet, lastUsePresetParts)) {
				if (partsSetTitle != null) {
					partsSetTitle += "*";
				}
			}
		}
		if (partsSetTitle != null && partsSetTitle.trim().length() > 0) {
			return partsSetTitle;
		}
		return defaultPartsSetName;
	}
	
	/**
	 * プレビューの更新を要求する.
	 * 更新は非同期に行われる.
	 */
	protected void requestPreview() {
		if (!characterData.isValid()) {
			return;
		}
		
		// 選択されているパーツの各イメージを取得しレイヤー順に並び替えて合成する.
		// 合成は別スレッドにて非同期に行われる.
		// リクエストは随時受け付けて、最新のリクエストだけが処理される.
		// (処理がはじまる前に新しいリクエストで上書きされた場合、前のリクエストは単に捨てられる.)
		imageBuilder.requestJob(new ImageBuildJobAbstractAdaptor(characterData) {

			/**
			 * 構築するパーツセット情報
			 */
			private PartsSet requestPartsSet;
			
			/**
			 * 非同期のイメージ構築要求の番号.<br>
			 */
			private long ticket;
			
			@Override
			public void onQueueing(long ticket) {
				this.ticket = ticket;
				previewPane.setLoadingRequest(ticket);
			}
			@Override
			public void buildImage(ImageOutput output) {
				// 合成結果のイメージを引数としてイメージビルダから呼び出される.
				final BufferedImage img = output.getImageOutput();
				final Color bgColor = output.getImageBgColor();
				Runnable refreshJob = new Runnable() {
					public void run() {
						previewPane.setImageBgColor(bgColor);
						previewPane.setPreviewImage(img);
						previewPane.setLoadingComplete(ticket);
						showPresetName(requestPartsSet);
					}
				};
				if (SwingUtilities.isEventDispatchThread()) {
					refreshJob.run();
				} else {
					try {
						SwingUtilities.invokeAndWait(refreshJob);
					} catch (Exception ex) {
						logger.log(Level.WARNING, "build image failed.", ex);
					}
				}
			}
			@Override
			public void handleException(final Throwable ex) {
				// 合成中に例外が発生した場合、イメージビルダから呼び出される.
				Runnable showExceptionJob = new Runnable() {
					public void run() {
						ErrorMessageHelper.showErrorDialog(MainFrame.this, ex);
					}
				};
				if (SwingUtilities.isEventDispatchThread()) {
					showExceptionJob.run();
				} else {
					SwingUtilities.invokeLater(showExceptionJob);
				}
			}
			@Override
			protected PartsSet getPartsSet() {
				// 合成できる状態になった時点でイメージビルダから呼び出される.
				final PartsSet[] result = new PartsSet[1];
				Runnable collectPartsSetJob = new Runnable() {
					public void run() {
						PartsSet partsSet = partsSelectionManager.createPartsSet();
						result[0] = partsSet;
					}
				};
				if (SwingUtilities.isEventDispatchThread()) {
					collectPartsSetJob.run();
				} else {
					try {
						// スレッドによるSwingのイベントディスパッチスレッド以外からの呼び出しの場合、
						// Swingディスパッチスレッドでパーツの選択状態を取得する.
						SwingUtilities.invokeAndWait(collectPartsSetJob);
						
					} catch (InvocationTargetException e) {
						throw new RuntimeException(e.getMessage(), e);
					} catch (InterruptedException e) {
						throw new RuntimeException("interrupted:" + e, e);
					}
				}
				if (logger.isLoggable(Level.FINE)) {
					logger.log(Level.FINE, "preview: " + result[0]);
				}
				requestPartsSet = result[0];
				return requestPartsSet;
			}
		});
	}
	
	/**
	 * プロファイルを開く
	 */
	protected void onOpenProfile() {
		try {
			MainFrame main2 = ProfileSelectorDialog.openProfile(this);
			if (main2 != null) {
				Point pt = getLocation();
				pt.x += 100;
				main2.setLocation(pt);
				main2.setVisible(true);
			}

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}

	/**
	 * 背景色を変更する.
	 */
	protected void onChangeBgColor() {
		getJMenuBar().setEnabled(false);
		try {
			Properties strings = LocalizedResourcePropertyLoader.getInstance()
					.getLocalizedProperties(STRINGS_RESOURCE);

			Color color = partsSelectionManager.getImageBgColor();
			color = JColorChooser.showDialog(this, strings.getProperty("chooseBgColor"), color);
			if (color != null) {
				partsSelectionManager.setImageBgColor(color);
				requestPreview();
			}
		} finally {
			getJMenuBar().setEnabled(true);
		}
	}
	
	/**
	 * プリビューしている画像をファイルに保存する。
	 * サポートしているのはPNG/JPEGのみ。
	 */
	protected void onSavePicture() {
		Toolkit tk = Toolkit.getDefaultToolkit();
		BufferedImage img = previewPane.getPreviewImage();
		Color imgBgColor = partsSelectionManager.getImageBgColor();
		if (img == null) {
			tk.beep();
			return;
		}
		
		try {
			File outFile = imageSaveHelper.showSaveFileDialog(this);
			if (outFile == null) {
				return;
			}
			
			StringBuilder warnings = new StringBuilder();

			setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
			try {
				imageSaveHelper.savePicture(img, imgBgColor, outFile, warnings);
			} finally {
				setCursor(Cursor.getDefaultCursor());
			}
			if (warnings.length() > 0) {
				JOptionPane.showMessageDialog(this, warnings.toString(), "WARNINGS", JOptionPane.WARNING_MESSAGE);
			}

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}

	/**
	 * パーツの管理ダイアログを開く.<br>
	 */
	protected void onManageParts() {
		if (!characterData.isValid()) {
			return;
		}
		
		PartsManageDialog mrgDlg = new PartsManageDialog(this, characterData);
		mrgDlg.setVisible(true);
		
		if (mrgDlg.isUpdated()) {
			// パーツ管理情報が更新された場合、
			// パーツデータをリロードする.
			if (characterData.reloadPartsData()) {
				partsSelectionManager.loadParts();
				requestPreview();
			}
		}
	}

	/**
	 * 「パーツ検索」ダイアログを新規に開く.<br>
	 * このダイアログは同時に複数開くことができる.<br>
	 */
	protected void onSearch() {
		if (!characterData.isValid()) {
			return;
		}
		
		SearchPartsDialog searchPartsDlg = new SearchPartsDialog(this, characterData, partsSelectionManager);
		searchPartsDlg.setVisible(true);
	}

	/**
	 * すべての「パーツ検索」ダイアログを閉じる.<br>
	 * パーツ検索ダイアログは同時に複数開けるので、開いている全部を閉じる.<br>
	 */
	protected void closeAllSearchDialog() {
		for (SearchPartsDialog dlg : SearchPartsDialog.getDialogs()) {
			if (dlg != null && dlg.isDisplayable() && dlg.getParent() == this) {
				dlg.dispose();
			}
		}
	}
	
	/**
	 * クリップボードにコピー
	 */
	protected void onCopy() {
		try {
			BufferedImage img = previewPane.getPreviewImage();
			Color imgBgColor = partsSelectionManager.getImageBgColor();
			if (img == null) {
				Toolkit tk = Toolkit.getDefaultToolkit();
				tk.beep();
				return;
			}
			
			ClipboardUtil.setImage(img, imgBgColor);

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	/**
	 * アプリケーションの設定ダイアログを開く
	 */
	protected void onPreferences() {
		AppConfigDialog appConfigDlg = new AppConfigDialog(this);
		appConfigDlg.setVisible(true);
	}

	/**
	 * ドロップターゲットドロップイベント
	 * @param dtde ドロップイベント
	 */
	protected void onDrop(DropTargetDropEvent dtde) {
		Toolkit tk = Toolkit.getDefaultToolkit();
		if (characterData == null || !characterData.isValid()) {
			tk.beep();
			return;
		}

		try {
			File dropFile = null;
			// ドロップされたものが1つのファイルであれば受け入れる。
			for (DataFlavor flavor : dtde.getCurrentDataFlavors()) {
				if (DataFlavor.javaFileListFlavor.equals(flavor)) {
					dtde.acceptDrop(DnDConstants.ACTION_COPY);
					@SuppressWarnings({ "unchecked", "rawtypes" })
					List<File> files = (List) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
					if (files.size() == 1){
						dropFile = files.get(0);
					}
					break;
				}
			}
			
			if (dropFile == null || !dropFile.exists()) {
				tk.beep();
				return;
			}

			// インポートダイアログを開く.
			// ドロップソースの処理がブロッキングしないように、
			// ドロップハンドラの処理を終了してからインポートダイアログが開くようにする.
			final File file = dropFile;
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					onImport(file);
				}
			});
			
		} catch (UnsupportedFlavorException ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
			
		} catch (IOException ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	/**
	 * インポートウィザードを実行する.<br>
	 * インポートが実行された場合は、パーツをリロードする.<br>
	 * インポートウィザード表示中は監視スレッドは停止される.<br>
	 * @param initFile アーカイブファィルまたはディレクトリ、指定がなければnull
	 */
	protected void onImport(File initFile) {
		// 監視スレッドを停止してからインポート処理を始める
		boolean stopped = watchAgent.stop();
		try {
			// インポートウィザードの実行
			ImportWizardDialog importWizDialog = new ImportWizardDialog(this, characterData, initFile);
			importWizDialog.setVisible(true);

			if (importWizDialog.getExitCode() == ImportWizardDialog.EXIT_PROFILE_UPDATED) {
				try {
					setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
					try {
						// インポートが実行された場合、パーツデータをリロードする.
						if (characterData.reloadPartsData()) {
							partsSelectionManager.loadParts();

							// パーツデータが変更された可能性があるので、
							// 監視状態をリセットしてから再開するようにする.
							watchAgent.reset();
						}
						
						// お気に入りをリロードする.
						CharacterDataPersistent persiste = CharacterDataPersistent.getInstance();
						persiste.loadFavorites(characterData);
					
					} finally {
						setCursor(Cursor.getDefaultCursor());
					}

				} catch (Exception ex) {
					ErrorMessageHelper.showErrorDialog(this, ex);
				}

				// 再表示
				requestPreview();
			}
			
		} finally {
			// 監視スレッドを再開する.
			if (stopped) {
				watchAgent.start();
			}
		}
	}
	
	protected void onExport() {
		ExportWizardDialog exportWizDlg = new ExportWizardDialog(this, characterData, previewPane.getPreviewImage());
		exportWizDlg.setVisible(true);
	}

	protected void onResetColor() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}
		
		Properties strings = LocalizedResourcePropertyLoader.getInstance()
				.getLocalizedProperties(STRINGS_RESOURCE);
		
		if (JOptionPane.showConfirmDialog(this, strings.get("confirm.resetcolors"), strings.getProperty("confirm"),
				JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) {
			return;
		}
		characterData.getPartsColorManager().resetPartsColorInfo();
		partsColorCoordinator.initColorDialog();
		requestPreview();
	}
	
	protected void onCloseProfile() {
		saveWorkingSet();
		unregisterUsedCharacterData(characterData);
		
		URL characterDocBase = characterData.getDocBase();
		if (characterDocBase != null) {

			// 最後に使用したキャラクターデータとして記憶する.
			RecentDataPersistent recentPersist = RecentDataPersistent.getInstance();
			try {
				recentPersist.saveRecent(characterData);
			} catch (Exception ex) {
				logger.log(Level.WARNING, "recent data saving failed.", ex);
				// recent情報の記録に失敗しても致命的ではないので、これは無視する.
			}
		}
		
		// スレッドを停止する.
		imageBuilder.stop();
		watchAgent.stop();

		// フレームウィンドウを破棄する.
		dispose();
		
		// 正常ログ
		logger.log(Level.INFO, "dispose mainframe.");
	}
	
	/**
	 * 開いている、すべてのプロファイルを閉じる
	 */
	public static void closeAllProfiles() {
		// ウィンドウが閉じられることでアクティブなフレームが切り替わる場合を想定し、
		// 現在のアクティブなウィンドウをあらかじめ記憶しておく
		MainFrame mainFrame = activedMainFrame;

		// gcをかけてファイナライズを促進させる
		SystemUtil.gc();
		
		// ファイナライズされていないFrameのうち、ネイティブリソースと関連づけられている
		// フレームについて、それがMainFrameのインスタンスであれば閉じる.
		// ただし、現在アクティブなものは除く
		for (Frame frame : JFrame.getFrames()) {
			try {
				if (frame.isDisplayable()) {
					// ネイティブリソースと関連づけられているフレーム
					if (frame instanceof MainFrame && frame != mainFrame) {
						// MainFrameのインスタンスであるので閉じる処理が可能.
						((MainFrame) frame).onCloseProfile();
					}
				}
			
			} catch (Exception ex) {
				logger.log(Level.SEVERE, "mainframe closing failed.", ex);
				// フレームを閉じるときに失敗した場合、通常、致命的問題だが
				// クローズ処理は継続しなければならない.
			}
		}
		
		// 現在アクティブなフレームを閉じる.
		// 最後に閉じることで「最後に使ったプロファイル」として記憶させる.
		if (activedMainFrame != null && activedMainFrame.isDisplayable()) {
			try {
				activedMainFrame.onCloseProfile();

			} catch (Exception ex) {
				logger.log(Level.SEVERE, "mainframe closing failed.", ex);
				// フレームを閉じるときに失敗した場合、通常、致命的問題だが
				// クローズ処理は継続しなければならない.
			}
		}
	}
	
	/**
	 * 画面の作業状態を保存するユーザーデータを取得する.
	 * @return ユーザーデータ
	 */
	protected UserData getWorkingSetUserData() {
		return getWorkingSetUserData(characterData);
	}
	
	/**
	 * 画面の作業状態を保存するユーザーデータを取得する.<br>
	 * キャラクターデータがnullまたは有効でない場合はnullを返す.<br>
	 * @param cd キャラクターデータ
	 * @return 作業状態を保存するユーザーデータ、もしくはnull
	 */
	public static UserData getWorkingSetUserData(CharacterData cd) {
		if (cd == null || !cd.isValid()) {
			return null;
		}
		String dataname = cd.getId() + "-workingset.ser";
		UserDataFactory userDataFactory = UserDataFactory.getInstance();
		UserData workingSetStore = userDataFactory
				.getMangledNamedUserData(cd.getDocBase(), dataname);
		return workingSetStore;
	}
	
	/**
	 * 画面の作業状態を保存する.
	 */
	protected void saveWorkingSet() {
		if (!characterData.isValid()) {
			return;
		}
		try {
			WorkingSet workingSet = new WorkingSet();
			workingSet.setCharacterDocBase(characterData.getDocBase());
			workingSet.setCharacterDataRev(characterData.getRev());
			PartsSet partsSet = partsSelectionManager.createPartsSet();
			workingSet.setPartsSet(partsSet);
			workingSet.setPartsColorInfoMap(characterData.getPartsColorManager().getPartsColorInfoMap());
			workingSet.setLastUsedSaveDir(imageSaveHelper.getLastUsedSaveDir());
			workingSet.setLastUsedExportDir(ExportWizardDialog.getLastUsedDir());
			workingSet.setLastUsePresetParts(lastUsePresetParts);

			UserData workingSetStore = getWorkingSetUserData();
			workingSetStore.save(workingSet);
			
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	/**
	 * 画面の作業状態を復元する.
	 * @return
	 */
	protected boolean loadWorkingSet() {
		if (!characterData.isValid()) {
			return false;
		}
		try {
			UserData workingSetStore = getWorkingSetUserData();
			if (workingSetStore != null && workingSetStore.exists()) {
				WorkingSet workingSet = (WorkingSet) workingSetStore.load();
				if (!characterData.getDocBase().toString().equals(workingSet.getCharacterDocBase().toString())) {
					// ワーキングセットのキャラクターデータとキャラクターデータを定義しているDocBaseが異なる場合はワーキングセットを無視する
					logger.info("workingset data mismatch:" + characterData);
					return false;
				}

				String docRev = characterData.getRev();
				String workRev = workingSet.getCharacterDataRev();
				if (docRev == null || workRev == null || !docRev.equals(workRev)) {
					// ワーキングセットが保存されてからキャラクターデータの定義が変更されている場合はワーキングセットは無視する.
					logger.info("workingset revision mismatch: actual=" + characterData + "/workingSet=" + workingSet);
					return false;
				}
				
				Map<PartsIdentifier, PartsColorInfo> partsColorInfoMap = characterData.getPartsColorManager().getPartsColorInfoMap();
				for (Map.Entry<PartsIdentifier, PartsColorInfo> entry : workingSet.getPartsColorInfoMap().entrySet()) {
					PartsIdentifier partsIdentifier = entry.getKey();
					PartsColorInfo partsColorInfo = entry.getValue();
					partsColorInfoMap.put(partsIdentifier, partsColorInfo);
				}
				
				// 選択されているパーツの復元
				PartsSet partsSet = workingSet.getPartsSet();
				selectPresetParts(partsSet.createCompatible(characterData));

				// 最後に選択したお気に入り情報の復元
				PartsSet lastUsePresetParts = workingSet.getLastUsePresetParts();
				if (lastUsePresetParts != null && lastUsePresetParts.hasName()
						&& lastUsePresetParts.isSameStructure(partsSet)) {
					this.lastUsePresetParts = lastUsePresetParts;
					showPresetName(lastUsePresetParts);
				}
				
				imageSaveHelper.setLastUseSaveDir(workingSet.getLastUsedSaveDir());
				ExportWizardDialog.setLastUsedDir(workingSet.getLastUsedExportDir());
				
				return true;
			}
			
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
		return false;
	}


	protected void onAbout() {
		try {
			AboutBox aboutBox = new AboutBox(this);
			aboutBox.showAboutBox();
			
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}

	protected void onHelp() {
		try {
			Properties strings = LocalizedResourcePropertyLoader.getInstance()
					.getLocalizedProperties(STRINGS_RESOURCE);
			String helpURL = strings.getProperty("help.url");

			URI helpURI = new URI(helpURL);

			if (!DesktopUtilities.browse(helpURI) ){
				// JDK5で実行中の場合

				JPanel panel = new JPanel();
				BoxLayout layout = new BoxLayout(panel, BoxLayout.Y_AXIS);
				panel.setLayout(layout);
				panel.add(new JLabel(strings.getProperty("help.show")));
				JTextField txtURL = new JTextField(helpURL);
				panel.add(txtURL);
				JOptionPane.showMessageDialog(this, panel);
			}

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	protected void onFlipHolizontal() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}

		double[] affineTransformParameter = partsSelectionManager.getAffineTransformParameter();
		if (affineTransformParameter == null) {
			// 左右フリップするアフィン変換パラメータを構築する.
			Dimension siz = characterData.getImageSize();
			if (siz != null) {
				affineTransformParameter = new double[] {-1., 0, 0, 1., siz.width, 0};
			}
		} else {
			// アフィン変換パラメータをクリアする.
			affineTransformParameter = null;
		}
		partsSelectionManager.setAffineTransformParameter(affineTransformParameter);
		requestPreview();
	}
	
	protected void onInformation() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}

		PartsSet partsSet = partsSelectionManager.createPartsSet();
		InformationDialog infoDlg = new InformationDialog(this, characterData, partsSet);
		infoDlg.setVisible(true);
	}
	
	protected void onManageFavorites() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}

		// お気に入りの状態を最新にリフレッシュする.
		refreshFavorites();

		// お気に入り編集ダイアログを開く
		ManageFavoriteDialog dlg = new ManageFavoriteDialog(this, characterData);
		dlg.setVisible(true);
		if (!dlg.isModified()) {
			return;
		}

		// お気に入りを登録する.
		try {
			setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
			try {
				CharacterDataPersistent persiste = CharacterDataPersistent.getInstance();
				persiste.saveFavorites(characterData);
				
				notifyChangeFavorites(characterData);

			} finally {
				setCursor(Cursor.getDefaultCursor());
			}

		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	/**
	 * 最新のお気に入りの状態を取り出す.<br>
	 */
	protected void refreshFavorites() {
		logger.log(Level.INFO, "refresh Favorites.: " + characterData);
		try {
			setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
			try {
				CharacterDataPersistent persiste = CharacterDataPersistent.getInstance();
				characterData.clearPartsSets(true);
				persiste.loadFavorites(characterData);
			} finally {
				setCursor(Cursor.getDefaultCursor());
			}

		} catch (Exception ex) {
			logger.log(Level.WARNING, "can't refresh favorites: " + characterData, ex);
		}
	}
	
	protected void onRegisterFavorite() {
		if (!characterData.isValid()) {
			Toolkit tk = Toolkit.getDefaultToolkit();
			tk.beep();
			return;
		}
		try {
			// パーツセットを生成
			PartsSet partsSet = partsSelectionManager.createPartsSet();
			if (partsSet.isEmpty()) {
				// 空のパーツセットは登録しない.
				return;
			}

			Properties strings = LocalizedResourcePropertyLoader.getInstance()
					.getLocalizedProperties(STRINGS_RESOURCE);
			
			// カラー情報の有無のチェックボックス.
			JCheckBox chkColorInfo = new JCheckBox(strings.getProperty("input.favoritesColorInfo"));
			chkColorInfo.setSelected(true);
			
			// お気に入りに登録するパーツセットが最後に使用したお気に入りと同じ構成であれば、
			// そのお気に入り名を使用する.
			String initName = getSuggestPartsSetName(partsSet, false, "");
			
			// 入力ダイアログを開く
			String name = (String) JOptionPane.showInputDialog(this, chkColorInfo,
					strings.getProperty("input.favorites"),
					JOptionPane.QUESTION_MESSAGE,
					null,
					null,
					initName);
			if (name == null || name.trim().length() == 0) {
				return;
			}

			boolean includeColorInfo = chkColorInfo.isSelected();
			if (!includeColorInfo) {
				// カラー情報を除去する.
				partsSet.removeColorInfo();
			}
			
			// IDを設定する.
			String partsSetId = "ps" + UUID.randomUUID().toString();
			partsSet.setPartsSetId(partsSetId);

			// 名前を設定する.
			partsSet.setLocalizedName(name);

			// ファイルに保存
			setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
			try {
				CharacterDataPersistent persiste = CharacterDataPersistent.getInstance();
				// 現在の最新情報を取り出す.
				characterData.clearPartsSets(true);
				persiste.loadFavorites(characterData);
				
				// お気に入りコレクションに登録
				characterData.addPartsSet(partsSet);

				persiste.saveFavorites(characterData);

				notifyChangeFavorites(characterData);
				
			} finally {
				setCursor(Cursor.getDefaultCursor());
			}
			
			// 最後に選択したお気に入りにする
			lastUsePresetParts = partsSet;
			showPresetName(partsSet);
			
		} catch (Exception ex) {
			ErrorMessageHelper.showErrorDialog(this, ex);
		}
	}
	
	/**
	 * すべての解除可能なパーツの選択を解除する。
	 */
	protected void onDeselectAll() {
		partsSelectionManager.deselectAll();
	}
	
	/**
	 * 単一選択カテゴリのパーツの解除を許可する。
	 */
	protected void onDeselectableAllCategory() {
		partsSelectionManager
				.setDeselectableSingleCategory(!partsSelectionManager
						.isDeselectableSingleCategory());
	}

	protected JMenuBar createMenuBar() {
		// JRE1.5ではアンチエイリアスをする.
		final boolean needAntiAlias = isNeedAntialias();
		logger.info("needAntiAlias=" + needAntiAlias);
		
		JMenuBar menuBar = new JMenuBar() {
			private static final long serialVersionUID = 1L;
			@Override
			public void paint(Graphics g) {
				if (needAntiAlias) {
					((Graphics2D) g).setRenderingHint(
							RenderingHints.KEY_TEXT_ANTIALIASING,
							RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
				}
				super.paint(g);
			}
		};

		Properties menuProps = LocalizedResourcePropertyLoader.getInstance()
				.getLocalizedProperties(MENU_STRINGS_RESOURCE);
		
		MenuDataFactory[] menus = new MenuDataFactory[] {
				new MenuDataFactory("menu.file", new MenuDataFactory[] {
						new MenuDataFactory("file.openProfile", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onOpenProfile();
							}
						}),
						new MenuDataFactory("file.savePicture", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onSavePicture();
							}
						}),
						new MenuDataFactory("file.manageParts", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onManageParts();
							}
						}),
						new MenuDataFactory("file.import", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onImport(null);
							};
						}),
						new MenuDataFactory("file.export", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onExport();
							};
						}),
						new MenuDataFactory("file.preferences", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onPreferences();
							};
						}),
						null,
						new MenuDataFactory("file.closeProfile", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onCloseProfile();
							}
						}),
				}),
				new MenuDataFactory("menu.edit", new MenuDataFactory[] {
						new MenuDataFactory("edit.search", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onSearch();
							}
						}),
						new MenuDataFactory("edit.copy", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onCopy();
							}
						}),
						new MenuDataFactory("edit.flipHorizontal", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onFlipHolizontal();
							}
						}),
						new MenuDataFactory("edit.resetcolor", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onResetColor();
							}
						}),
						null,
						new MenuDataFactory("edit.information", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onInformation();
							}
						}),
						null,
						new MenuDataFactory("edit.deselectall", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onDeselectAll();
							}
						}),
						new MenuDataFactory("edit.deselectparts", true, new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onDeselectableAllCategory();
							}
						}),
				}),
				new MenuDataFactory("menu.favorite", new MenuDataFactory[] {
						new MenuDataFactory("favorite.register", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onRegisterFavorite();
							}
						}),
						new MenuDataFactory("favorite.manage", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onManageFavorites();
							}
						}),
						null,
				}),
				new MenuDataFactory("menu.help", new MenuDataFactory[] {
						new MenuDataFactory("help.help", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onHelp();
							}
						}),
						new MenuDataFactory("help.about", new ActionListener() {
							public void actionPerformed(ActionEvent e) {
								onAbout();
							}
						}),
				}), };
		
		final HashMap<String, JMenu> menuMap = new HashMap<String, JMenu>();
		final HashMap<String, JMenuItem> menuItemMap = new HashMap<String, JMenuItem>();

		for (MenuDataFactory menuDataFactory : menus) {
			MenuData menuData = menuDataFactory.createMenuData(menuProps);
			JMenu menu = new JMenu() {
				private static final long serialVersionUID = 1L;
				@Override
				public void paint(Graphics g) {
					if (needAntiAlias) {
						((Graphics2D) g).setRenderingHint(
								RenderingHints.KEY_TEXT_ANTIALIASING,
								RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
					}
					super.paint(g);
				}
			};
			if (menuData.makeMenu(menu)) {
				menuBar.add(menu);
				
				menuMap.put(menuDataFactory.getName(), menu);
				
				for (MenuData child : menuData) {
					if (child == null) {
						menu.add(new JSeparator());
					} else {
						JMenuItem menuItem;
						if (child.isCheckbox()) {
							menuItem = new JCheckBoxMenuItem() {
								private static final long serialVersionUID = 1L;
								@Override
								public void paint(Graphics g) {
									if (needAntiAlias) {
										((Graphics2D) g).setRenderingHint(
												RenderingHints.KEY_TEXT_ANTIALIASING,
												RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
									}
									super.paint(g);
								}
							};
						} else {
							menuItem = new JMenuItem() {
								private static final long serialVersionUID = 1L;
								@Override
								public void paint(Graphics g) {
									if (needAntiAlias) {
										((Graphics2D) g).setRenderingHint(
												RenderingHints.KEY_TEXT_ANTIALIASING,
												RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
									}
									super.paint(g);
								}
							};
						}
						if (child.makeMenu(menuItem)) {
							menu.add(menuItem);
							menuItemMap.put(child.getName(), menuItem);
						}
					}
				}
			}
		}
		
		menuMap.get("menu.edit").addMenuListener(new MenuListener() {
			public void menuCanceled(MenuEvent e) {
				// do nothing.
			}
			public void menuDeselected(MenuEvent e) {
				// do nothing.
			}
			public void menuSelected(MenuEvent e) {
				menuItemMap.get("edit.copy").setEnabled(previewPane.getPreviewImage() != null);
				menuItemMap.get("edit.deselectparts").setSelected(
						partsSelectionManager.isDeselectableSingleCategory());
			}
		});
		menuMap.get("menu.favorite").addMenuListener(new MenuListener() {
			public void menuCanceled(MenuEvent e) {
				// do nothing.
			}
			public void menuDeselected(MenuEvent e) {
				// do nothing.
			}
			public void menuSelected(MenuEvent e) {
				JMenu menu = menuMap.get("menu.favorite");
				onSelectedFavoriteMenu(menu);
			}
		});
		
		return menuBar;
	}

	/**
	 * メニュー項目のアンチエイリアスが必要か判定する.<br>
	 * java.specification.versionが1.5で始まる場合は必要とみなす.<br>
	 * @return アンチエイリアスが必要であればtrue
	 */
	private boolean isNeedAntialias() {
		return System.getProperty("java.specification.version").startsWith("1.5");
	}
	
}
