OSDN Git Service

AppConfig設定ダイアログでの編集方法を文字列ではなくデータ型で編集できるように刷新した。
[charactermanaj/CharacterManaJ.git] / src / main / java / charactermanaj / ui / AppConfigDialog.java
index 6acb5f1..a03d834 100644 (file)
@@ -4,24 +4,32 @@ import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
 import java.awt.Container;
+import java.awt.Dimension;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
 import java.awt.Toolkit;
 import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseEvent;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import javax.swing.AbstractAction;
+import javax.swing.AbstractCellEditor;
 import javax.swing.Action;
 import javax.swing.ActionMap;
 import javax.swing.BorderFactory;
@@ -29,6 +37,7 @@ import javax.swing.Box;
 import javax.swing.InputMap;
 import javax.swing.JButton;
 import javax.swing.JCheckBox;
+import javax.swing.JColorChooser;
 import javax.swing.JComponent;
 import javax.swing.JDialog;
 import javax.swing.JFrame;
@@ -39,12 +48,20 @@ import javax.swing.JRootPane;
 import javax.swing.JScrollPane;
 import javax.swing.JTable;
 import javax.swing.KeyStroke;
+import javax.swing.SwingConstants;
+import javax.swing.event.TableModelEvent;
+import javax.swing.event.TableModelListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableCellEditor;
 import javax.swing.table.TableCellRenderer;
 import javax.swing.table.TableColumnModel;
 
 import charactermanaj.Main;
 import charactermanaj.model.AppConfig;
-import charactermanaj.ui.model.AbstractTableModelWithComboBoxModel;
+import charactermanaj.util.BeanPropertiesUtilities;
+import charactermanaj.util.BeanPropertiesUtilities.PropertyAccessor;
+import charactermanaj.util.BeanPropertiesUtilities.PropertyAccessorMap;
 import charactermanaj.util.ConfigurationDirUtilities;
 import charactermanaj.util.DesktopUtilities;
 import charactermanaj.util.ErrorMessageHelper;
@@ -54,23 +71,256 @@ import charactermanaj.util.SetupLocalization;
 
 /**
  * アプリケーション設定ダイアログ
- * 
+ *
  * @author seraphy
  */
 public class AppConfigDialog extends JDialog {
 
        private static final long serialVersionUID = 1L;
-       
+
        private static final Logger logger = Logger.getLogger(AppConfigDialog.class.getName());
 
        private AppConfigTableModel appConfigTableModel;
-       
+
        private JTable appConfigTable;
-       
+
        private JCheckBox chkResetDoNotAskAgain;
-       
+
        private RecentCharactersDir recentCharactersDir;
-       
+
+       private AbstractAction actApply;
+
+       private boolean orgDoNotAskAgain;
+
+       public enum ColumnDef {
+               NAME("column.key", String.class, false) {
+                       @Override
+                       public Object getValue(AppConfigRow row) {
+                               return row.getDisplayName();
+                       }
+               },
+               VALUE("column.value", String.class, true) {
+                       @Override
+                       public Object getValue(AppConfigRow row) {
+                               return row.getValue();
+                       }
+                       @Override
+                       public void setValue(AppConfigRow row, Object value) {
+                               row.setValue(value);
+                       }
+               };
+
+               private final String reskey;
+
+               private final Class<?> type;
+
+               private final boolean editable;
+
+               ColumnDef(String reskey, Class<?> type, boolean editable) {
+                       this.reskey = reskey;
+                       this.type = type;
+                       this.editable = editable;
+               }
+
+               public boolean isEditable() {
+                       return editable;
+               }
+
+               public String getResourceKey() {
+                       return reskey;
+               }
+
+               public Class<?> getType() {
+                       return type;
+               }
+
+               public abstract Object getValue(AppConfigRow row);
+
+               public void setValue(AppConfigRow row, Object value) {
+                       throw new UnsupportedOperationException(name());
+               }
+       }
+
+       private static class AppConfigRow {
+
+               private final String name;
+
+               private final PropertyAccessor accessor;
+
+               private String order = "";
+
+               private String displayName;
+
+               private Object orgValue;
+
+               private Object value;
+
+               private boolean rejected;
+
+               public AppConfigRow(String name, PropertyAccessor accessor, Object value) {
+                       this.name = name;
+                       this.accessor = accessor;
+                       this.value = value;
+                       this.orgValue = value;
+               }
+
+               public String getName() {
+                       return name;
+               }
+
+               public Class<?> getPropertyType() {
+                       Class<?> dataType = accessor.getPropertyType();
+                       // JTableのセルレンダラーではプリミティブ型の編集は対応していないので
+                       // ラッパー型に置き換える
+                       if (dataType.isPrimitive()) {
+                               if (dataType.equals(int.class)) {
+                                       dataType = Integer.class;
+                               } else if (dataType.equals(long.class)) {
+                                       dataType = Long.class;
+                               } else if (dataType.equals(float.class)) {
+                                       dataType = Float.class;
+                               } else if (dataType.equals(double.class)) {
+                                       dataType = Double.class;
+                               } else if (dataType.equals(boolean.class)) {
+                                       dataType = Boolean.class;
+                               }
+                       }
+                       return dataType;
+               }
+
+               public String getOrder() {
+                       return order;
+               }
+
+               public void setOrder(String order) {
+                       if (order == null) {
+                               order = "";
+                       }
+                       this.order = order;
+               }
+
+               public String getDisplayName() {
+                       return (displayName == null || displayName.length() == 0) ? name : displayName;
+               }
+
+               public void setDisplayName(String displayName) {
+                       this.displayName = displayName;
+               }
+
+               public Object getValue() {
+                       return value;
+               }
+
+               public void setValue(Object value) {
+                       this.value = value;
+               }
+
+               public boolean isRejected() {
+                       return rejected;
+               }
+
+               public void setRejected(boolean rejected) {
+                       this.rejected = rejected;
+               }
+
+               public boolean isModified() {
+                       return orgValue == null ? value != null : !orgValue.equals(value);
+               }
+       }
+
+       private static class AppConfigTableModel extends AbstractTableModel {
+
+               private static final long serialVersionUID = 1L;
+
+               protected static final ColumnDef[] COLUMNS = ColumnDef.values();
+
+               private List<AppConfigRow> items = Collections.emptyList();
+
+               public List<AppConfigRow> getItems() {
+                       return items;
+               }
+
+               public void setItems(List<AppConfigRow> items) {
+                       if (items == null) {
+                               items = Collections.emptyList();
+                       }
+                       this.items = items;
+                       fireTableDataChanged();
+               }
+
+               public void setRejectNames(Set<String> rejectNames) {
+                       if (rejectNames == null) {
+                               rejectNames = Collections.emptySet();
+                       }
+                       for (AppConfigRow item : items) {
+                               String key = item.getName();
+                               boolean rejected = rejectNames.contains(key);
+                               item.setRejected(rejected);
+                       }
+                       fireTableDataChanged();
+               }
+
+               /**
+                * 編集されているか?
+                *
+                * @return 編集されていればtrue、そうでなければfalse
+                */
+               public boolean isModified() {
+                       return false;
+               }
+
+
+               @Override
+               public int getRowCount() {
+                       return items.size();
+               }
+
+               public int getColumnCount() {
+                       return COLUMNS.length;
+               }
+
+               @Override
+               public Class<?> getColumnClass(int columnIndex) {
+                       return COLUMNS[columnIndex].getType();
+               }
+
+               @Override
+               public String getColumnName(int column) {
+                       Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
+                                       .getLocalizedProperties("languages/appconfigdialog");
+                       String reskey = COLUMNS[column].getResourceKey();
+                       return strings.getProperty(reskey, reskey);
+               }
+
+               @Override
+               public boolean isCellEditable(int rowIndex, int columnIndex) {
+                       return COLUMNS[columnIndex].isEditable();
+               }
+
+               public Object getValueAt(int rowIndex, int columnIndex) {
+                       AppConfigRow row = items.get(rowIndex);
+                       return COLUMNS[columnIndex].getValue(row);
+               }
+
+               @Override
+               public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
+                       AppConfigRow row = items.get(rowIndex);
+                       COLUMNS[columnIndex].setValue(row, aValue);
+                       fireTableRowsUpdated(rowIndex, rowIndex);
+               }
+
+               public void adjustColumnModel(TableColumnModel columnModel) {
+                       Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
+                                       .getLocalizedProperties("languages/appconfigdialog");
+                       int mx = columnModel.getColumnCount();
+                       for (int idx = 0; idx < mx; idx++) {
+                               String reskey = COLUMNS[idx].getResourceKey() + ".width";
+                               int width = Integer.parseInt(strings.getProperty(reskey));
+                               columnModel.getColumn(idx).setPreferredWidth(width);
+                       }
+               }
+       }
+
        public AppConfigDialog(JFrame parent) {
                super(parent, true);
                try {
@@ -83,21 +333,21 @@ public class AppConfigDialog extends JDialog {
                        });
 
                        initComponent();
-                       
+
                        loadData();
-                       
+
                } catch (RuntimeException ex) {
                        logger.log(Level.SEVERE, "appConfig construct failed.", ex);
                        dispose();
                        throw ex;
                }
        }
-       
+
        private void initComponent() {
-               
+
                Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
                                .getLocalizedProperties("languages/appconfigdialog");
-               
+
                setTitle(strings.getProperty("title"));
 
                Container contentPane = getContentPane();
@@ -108,10 +358,10 @@ public class AppConfigDialog extends JDialog {
                btnPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 45));
                GridBagLayout btnPanelLayout = new GridBagLayout();
                btnPanel.setLayout(btnPanelLayout);
-               
+
                GridBagConstraints gbc = new GridBagConstraints();
-               
-               Action actApply = new AbstractAction(strings.getProperty("btn.apply")) {
+
+               actApply = new AbstractAction(strings.getProperty("btn.apply")) {
                        private static final long serialVersionUID = 1L;
                        public void actionPerformed(ActionEvent e) {
                                onUpdate();
@@ -129,8 +379,15 @@ public class AppConfigDialog extends JDialog {
                                onSetupLocalization();
                        }
                };
-               
+
                chkResetDoNotAskAgain = new JCheckBox(strings.getProperty("chk.askForCharactersDir"));
+               chkResetDoNotAskAgain.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               // 保存ボタンの状態更新のため
+                               updateUIState();
+                       }
+               });
 
                gbc.gridx = 0;
                gbc.gridy = 0;
@@ -144,7 +401,7 @@ public class AppConfigDialog extends JDialog {
                gbc.weightx = 1.;
                gbc.weighty = 0.;
                btnPanel.add(chkResetDoNotAskAgain, gbc);
-               
+
                gbc.gridx = 0;
                gbc.gridy = 1;
                gbc.gridheight = 1;
@@ -166,68 +423,152 @@ public class AppConfigDialog extends JDialog {
                gbc.weightx = 1.;
                gbc.weighty = 0.;
                btnPanel.add(Box.createHorizontalGlue(), gbc);
-               
+
                gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 1;
                gbc.weightx = 0.;
                JButton btnApply = new JButton(actApply);
                btnPanel.add(btnApply, gbc);
-               
+
                gbc.gridx = Main.isLinuxOrMacOSX() ? 1 : 2;
                gbc.weightx = 0.;
                JButton btnCancel = new JButton(actCancel);
                btnPanel.add(btnCancel, gbc);
-               
+
                add(btnPanel, BorderLayout.SOUTH);
-               
-               setSize(350, 400);
+
+               setSize(400, 400);
                setLocationRelativeTo(getParent());
-               
+
                // Notes
                JLabel lblCaution = new JLabel(strings.getProperty("caution"), JLabel.CENTER);
                lblCaution.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
                lblCaution.setForeground(Color.red);
                contentPane.add(lblCaution, BorderLayout.NORTH);
-               
+
                // Model
                appConfigTableModel = new AppConfigTableModel();
 
                // JTable
                AppConfig appConfig = AppConfig.getInstance();
                final Color invalidBgColor = appConfig.getInvalidBgColor();
+
                appConfigTable = new JTable(appConfigTableModel) {
                        private static final long serialVersionUID = 1L;
+
                        @Override
                        public Component prepareRenderer(TableCellRenderer renderer,
                                        int row, int column) {
                                Component comp = super.prepareRenderer(renderer, row, column);
-                               AppConfigRowModel rowModel = appConfigTableModel.getRow(row);
-                               if (rowModel.isRejected()) {
+                               AppConfigRow configRow = appConfigTableModel.getItems().get(row);
+                               if (configRow.isRejected()) {
+                                       // 差し戻された項目は警告色とする
                                        comp.setBackground(invalidBgColor);
                                } else {
+                                       // そうでなければ標準色に戻す
                                        if (isCellSelected(row, column)) {
                                                comp.setBackground(getSelectionBackground());
                                        } else {
                                                comp.setBackground(getBackground());
                                        }
                                }
+
+                               if (configRow.isModified() && !configRow.isRejected()) {
+                                       // 変更行の色を警告色にする
+                                       // (ただし、rejectのものは背景色を変えているので何もしない)
+                                       comp.setForeground(invalidBgColor);
+                               } else {
+                                       // そうでなければ標準色に戻す
+                                       comp.setForeground(getForeground());
+                               }
                                return comp;
                        }
+
                        @Override
                        public String getToolTipText(MouseEvent event) {
                                int row = rowAtPoint(event.getPoint());
                                int col = columnAtPoint(event.getPoint());
-                               if (col == 0) {
-                                       AppConfigRowModel rowModel = appConfigTableModel.getRow(row);
-                                       return rowModel.getDisplayName();
+                               if (AppConfigTableModel.COLUMNS[col] == ColumnDef.NAME) {
+                                       // 最初の列の表示をツールチップとして表示させる
+                                       int modelRow = convertRowIndexToModel(row);
+                                       return appConfigTableModel.getItems().get(modelRow).getDisplayName();
                                }
                                return super.getToolTipText(event);
                        }
+
+                       // 1つの列で複数のデータタイプの編集を可能にする
+                       // Jtable with different types of cells depending on data type
+                       // https://stackoverflow.com/questions/16970824/jtable-with-different-types-of-cells-depending-on-data-type
+
+                       private Class<?> editingClass;
+
+                       @Override
+                       public TableCellRenderer getCellRenderer(int row, int column) {
+                               editingClass = null;
+                               int modelColumn = convertColumnIndexToModel(column);
+                               if (AppConfigTableModel.COLUMNS[modelColumn] == ColumnDef.VALUE) {
+                                       // VALUE列の場合
+                                       int modelRow = convertRowIndexToModel(row);
+                                       AppConfigRow rowData = appConfigTableModel.getItems().get(modelRow);
+
+                                       // 行のデータ型に対応するレンダラーを取得する
+                                       Class<?> dataType = rowData.getPropertyType();
+                                       TableCellRenderer renderer = getDefaultRenderer(dataType);
+                                       if (renderer != null) {
+                                               return renderer;
+                                       }
+                               }
+                               // VALUE列以外は、標準のまま (もしくはレンダラーがみつからない場合)
+                               return super.getCellRenderer(row, column);
+                       }
+
+                       @Override
+                       public TableCellEditor getCellEditor(int row, int column) {
+                               editingClass = null;
+                               int modelColumn = convertColumnIndexToModel(column);
+                               if (AppConfigTableModel.COLUMNS[modelColumn] == ColumnDef.VALUE) {
+                                       // VALUE列の場合
+                                       int modelRow = convertRowIndexToModel(row);
+                                       AppConfigRow rowData = appConfigTableModel.getItems().get(modelRow);
+
+                                       // 行のデータ型に対応するレンダラーを取得する
+                                       editingClass = rowData.getPropertyType();
+                                       return getDefaultEditor(editingClass);
+
+                               } else {
+                                       // VALUE列以外は、標準のまま
+                                       return super.getCellEditor(row, column);
+                               }
+                       }
+
+                       //  This method is also invoked by the editor when the value in the editor
+                       //  component is saved in the TableModel. The class was saved when the
+                       //  editor was invoked so the proper class can be created.
+
+                       @Override
+                       public Class<?> getColumnClass(int column) {
+                               return editingClass != null ? editingClass : super.getColumnClass(column);
+                       }
                };
-               appConfigTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
+
                appConfigTable.setShowGrid(true);
-               appConfigTable.setGridColor(AppConfig.getInstance().getGridColor());
+               appConfigTable.setGridColor(appConfig.getGridColor());
                appConfigTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
+               appConfigTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+
+               // データタイプがColorの場合のセルレンダラーとエディタを設定する
+               appConfigTable.setDefaultRenderer(Color.class, new ColorCellRender());
+               appConfigTable.setDefaultEditor(Color.class, new ColorCellEditor());
+
                appConfigTableModel.adjustColumnModel(appConfigTable.getColumnModel());
+
+               appConfigTableModel.addTableModelListener(new TableModelListener() {
+                       @Override
+                       public void tableChanged(TableModelEvent e) {
+                               // テーブルが変更された場合、保存ボタンの活性やReject状態の変更のため
+                               updateUIState();
+                       }
+               });
+
                JScrollPane appConfigTableSP = new JScrollPane(appConfigTable);
                appConfigTableSP.setBorder(BorderFactory.createCompoundBorder(
                                BorderFactory.createEmptyBorder(0, 3, 0, 3),
@@ -235,7 +576,7 @@ public class AppConfigDialog extends JDialog {
                                );
                appConfigTableSP.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
                contentPane.add(appConfigTableSP, BorderLayout.CENTER);
-               
+
                // RootPane
                Toolkit tk = Toolkit.getDefaultToolkit();
                JRootPane rootPane = getRootPane();
@@ -245,21 +586,101 @@ public class AppConfigDialog extends JDialog {
                im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeAppConfigDialog");
                am.put("closeAppConfigDialog", actCancel);
 
+               // 保存ボタンの活性制御
+               updateUIState();
+       }
+
+       /**
+        * 保存ボタンの活性制御とテーブルの編集状態の表示の更新
+        * (テーブルが編集された場合に更新される)
+        */
+       protected void updateUIState() {
+               boolean hasModified = false;
+
+               // テーブルを走査して変更を確認する
+               for (AppConfigRow itemRow : appConfigTableModel.getItems()) {
+                       if (itemRow.isModified()) {
+                               // 変更あり
+                               hasModified = true;
+                       } else if (itemRow.isRejected()) {
+                               // 変更されていない状態であれば、差し戻し状態を解除する
+                               itemRow.setRejected(false);
+                       }
+               }
+
+               // キャラクターデータディレクトリの問い合わせ状態が変わっているか?
+               if (orgDoNotAskAgain != chkResetDoNotAskAgain.isSelected()) {
+                       hasModified = true;
+               }
+
                // 保存先が無効であれば適用ボタンを有効にしない.
+               AppConfig appConfig = AppConfig.getInstance();
                boolean enableSave = !appConfig.getPrioritySaveFileList().isEmpty();
-               btnApply.setEnabled(enableSave);
+
+               // 保存が有効であり、且つ、変更された行があれば保存ボタンを有効とする
+               actApply.setEnabled(enableSave && hasModified);
        }
-       
+
        private void loadData() {
-               Properties original = AppConfig.getInstance().getProperties();
-               appConfigTableModel.initModel(original);
 
+               Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
+                               .getLocalizedProperties("languages/appconfigdialog");
+
+               // AppConfigへのアクセッサを取得する
+               PropertyAccessorMap accessorMap = BeanPropertiesUtilities.getPropertyAccessorMap(AppConfig.class);
+
+               AppConfig appConfig = AppConfig.getInstance();
+               accessorMap.setBean(appConfig);
+
+               List<AppConfigRow> items = new ArrayList<AppConfigRow>();
+               int fallbackOrder = 1000;
+               for (Map.Entry<String, PropertyAccessor> accessorEntry : accessorMap.entrySet()) {
+                       // プロパティ名と現在値を取得する
+                       String name = accessorEntry.getKey();
+                       PropertyAccessor accessor = accessorEntry.getValue();
+                       Object value = accessor.getValue();
+
+                       // リソースからプロパティ名に対応する表示名を取得する(なければプロパティ名のまま)
+                       String displayName = strings.getProperty(name, name);
+                       int pt = displayName.indexOf(";");
+                       String order = Integer.toString(fallbackOrder++);
+                       if (pt > 0) {
+                               order = displayName.substring(0, pt);
+                               displayName = displayName.substring(pt + 1);
+                       }
+
+                       // 行オブジェクト作成
+                       AppConfigRow rowItem = new AppConfigRow(name, accessor, value);
+                       rowItem.setDisplayName(displayName);
+                       rowItem.setOrder(order);
+
+                       items.add(rowItem);
+               }
+
+               // 表示順に並べる
+               Collections.sort(items, new Comparator<AppConfigRow>() {
+                       @Override
+                       public int compare(AppConfigRow o1, AppConfigRow o2) {
+                               int ret = o1.getOrder().compareTo(o2.getOrder());
+                               if (ret == 0) {
+                                       ret = o1.getDisplayName().compareTo(o2.getDisplayName());
+                               }
+                               if (ret == 0) {
+                                       ret = o1.getName().compareTo(o2.getName());
+                               }
+                               return ret;
+                       }
+               });
+
+               appConfigTableModel.setItems(items);
+
+               // 最後に使ったキャラクターデータディレクトリの自動選択設定
                try {
                        recentCharactersDir = RecentCharactersDir.load();
 
                        if (recentCharactersDir != null) {
                                File lastUseCharactersDir = recentCharactersDir.getLastUseCharacterDir();
-                               boolean enableLastUseCharacterDir = lastUseCharactersDir != null && lastUseCharactersDir.isDirectory(); 
+                               boolean enableLastUseCharacterDir = lastUseCharactersDir != null && lastUseCharactersDir.isDirectory();
                                boolean doNotAskAgain = enableLastUseCharacterDir && recentCharactersDir.isDoNotAskAgain();
                                chkResetDoNotAskAgain.setEnabled(enableLastUseCharacterDir);
                                chkResetDoNotAskAgain.setSelected(!doNotAskAgain);
@@ -269,6 +690,9 @@ public class AppConfigDialog extends JDialog {
                        recentCharactersDir = null;
                        logger.log(Level.WARNING, "RecentCharactersDir load failed.", ex);
                }
+
+               // 初期状態の保存
+               this.orgDoNotAskAgain = chkResetDoNotAskAgain.isSelected();
        }
 
        /**
@@ -283,7 +707,7 @@ public class AppConfigDialog extends JDialog {
                                JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.OK_OPTION) {
                        return;
                }
-               
+
                try {
                        File baseDir = ConfigurationDirUtilities.getUserDataDir();
                        SetupLocalization setup = new SetupLocalization(baseDir);
@@ -297,7 +721,7 @@ public class AppConfigDialog extends JDialog {
                        ErrorMessageHelper.showErrorDialog(this, ex);
                }
        }
-       
+
        protected void onClose() {
                if (appConfigTableModel.isModified()) {
                        Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
@@ -310,266 +734,257 @@ public class AppConfigDialog extends JDialog {
                }
                dispose();
        }
-       
+
+       /**
+        * AppConfigと、キャラクターデータディレクトリの起動時の選択有無の設定値を保存する。
+        */
        protected void onUpdate() {
-               
+
                if (appConfigTable.isEditing()) {
                        // 編集中ならば許可しない.
                        Toolkit tk = Toolkit.getDefaultToolkit();
                        tk.beep();
                        return;
                }
-               
+
                Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
                                .getLocalizedProperties("languages/appconfigdialog");
 
-               // 編集されたプロパティを取得する.
-               Properties props = appConfigTableModel.getProperties();
+               // 編集されたAppConfigの設定値を取得する. (変更のあるもののみ)
+               Map<String, Object> modifiedValues = new HashMap<String, Object>();
+               for (AppConfigRow rowItem : appConfigTableModel.getItems()) {
+                       if (rowItem.isModified()) {
+                               String name = rowItem.getName();
+                               Object value = rowItem.getValue();
+                               modifiedValues.put(name, value);
+                       }
+               }
 
-               // 編集されたプロパティが適用可能か検証する.
-               Set<String> rejectNames = AppConfig.checkProperties(props);
-               if (!rejectNames.isEmpty()) {
-                       // エラーがある場合
-                       appConfigTableModel.setRejectNames(rejectNames);
+               // キャラクターデータディレクトリの起動時の選択状態の変更状態
+               boolean updateRecentCharactersDir = (orgDoNotAskAgain != chkResetDoNotAskAgain.isSelected());
 
-                       JOptionPane.showMessageDialog(this, strings.getProperty("error.message"),
-                                       strings.getProperty("error.caption"), JOptionPane.ERROR_MESSAGE);
+               if (!updateRecentCharactersDir && modifiedValues.isEmpty()) {
+                       // 変更点がないので何もしない
                        return;
                }
 
-               try {
-                       // アプリケーション設定を更新し、保存する.
-                       AppConfig appConfig = AppConfig.getInstance();
-                       appConfig.update(props);
-                       appConfig.saveConfig();
-
-                       // キャラクターデータディレクトリの起動時の選択
-                       if (chkResetDoNotAskAgain.isEnabled()) {
-                               boolean doNotAskAgain = !chkResetDoNotAskAgain.isSelected();
-                               if (doNotAskAgain != recentCharactersDir.isDoNotAskAgain()) {
-                                       recentCharactersDir.setDoNotAskAgain(doNotAskAgain);
-                                       recentCharactersDir.saveRecents();
-                               }
+               // AppConfigの保存
+               if (!modifiedValues.isEmpty()) {
+                       // 編集されたプロパティが適用可能か検証する.
+                       Set<String> rejectNames = AppConfig.checkProperties(modifiedValues);
+                       if (!rejectNames.isEmpty()) {
+                               // エラーがある場合
+                               appConfigTableModel.setRejectNames(rejectNames);
+
+                               JOptionPane.showMessageDialog(this, strings.getProperty("error.message"),
+                                               strings.getProperty("error.caption"), JOptionPane.ERROR_MESSAGE);
+                               return;
                        }
 
-               } catch (Exception ex) {
-                       ErrorMessageHelper.showErrorDialog(this, ex);
-                       return;
+                       try {
+                               // アプリケーション設定を更新し、保存する.
+                               AppConfig appConfig = AppConfig.getInstance();
+                               appConfig.update(modifiedValues);
+                               appConfig.saveConfig();
+
+                       } catch (Exception ex) {
+                               ErrorMessageHelper.showErrorDialog(this, ex);
+                               return;
+                       }
                }
-               
+
+               // キャラクターデータディレクトリの起動時の選択の保存
+               if (updateRecentCharactersDir) {
+                       try {
+                               if (chkResetDoNotAskAgain.isEnabled()) {
+                                       boolean doNotAskAgain = !chkResetDoNotAskAgain.isSelected();
+                                       if (doNotAskAgain != recentCharactersDir.isDoNotAskAgain()) {
+                                               recentCharactersDir.setDoNotAskAgain(doNotAskAgain);
+                                               recentCharactersDir.saveRecents();
+                                       }
+                               }
+                       } catch (Exception ex) {
+                               ErrorMessageHelper.showErrorDialog(this, ex);
+                               return;
+                       }
+               }
+
                // アプリケーションの再起動が必要なことを示すダイアログを表示する.
                String message = strings.getProperty("caution");
                JOptionPane.showMessageDialog(this, message);
 
                dispose();
        }
-       
 }
 
-class AppConfigRowModel implements Comparable<AppConfigRowModel> {
-
-       private Properties target;
-       
-       private String key;
-       
-       private String displayName;
-       
-       private boolean rejected;
-       
-       public AppConfigRowModel(Properties target, String key, String displayName) {
-               this.target = target;
-               this.key = key;
-               this.displayName = displayName;
+/**
+ * カラーセル
+ */
+class ColorCell extends JPanel {
+       private static final long serialVersionUID = 1L;
+
+       private String title = "Color";
+
+       private JPanel box;
+
+       private JLabel label;
+
+       private JButton button;
+
+       private ActionListener actionListener;
+
+       public ColorCell() {
+               this(null);
        }
 
-       @Override
-       public int hashCode() {
-               return key == null ? 0 : key.hashCode();
+       /**
+        * ボタンのアクションリスナを指定して構築する
+        * @param actionListener
+        */
+       public ColorCell(ActionListener actionListener) {
+               super(new BorderLayout());
+               this.actionListener = actionListener;
+
+               box = new JPanel(new BorderLayout());
+
+               label = new JLabel();
+               label.setHorizontalAlignment(SwingConstants.CENTER);
+               box.add(label, BorderLayout.CENTER);
+               box.setBorder(BorderFactory.createEtchedBorder());
+
+               button = new JButton();
+               Dimension dim = button.getPreferredSize();
+               dim.width = 24;
+               button.setPreferredSize(dim);
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               onClick(e);
+                       }
+               });
+
+               add(box, BorderLayout.CENTER);
+               add(button, BorderLayout.EAST);
+               setSelectedColor(Color.BLACK);
        }
-       
-       @Override
-       public boolean equals(Object obj) {
-               if (obj == this) {
-                       return true;
-               }
-               if (obj != null && obj instanceof AppConfigRowModel) {
-                       AppConfigRowModel o = (AppConfigRowModel) obj;
-                       return key == null ? o.key == null : key.equals(o.key);
-               }
-               return false;
+
+       public String getTitle() {
+               return title;
        }
-       
-       public int compareTo(AppConfigRowModel o) {
-               if (o == this) {
-                       return 0;
-               }
-               int ret = displayName.compareTo(o.displayName);
-               if (ret == 0) {
-                       ret = key.compareTo(o.key);
+
+       public void setTitle(String title) {
+               String old = this.title;
+               if (old == null ? title != null : !old.equals(title)) {
+                       this.title = title;
+                       firePropertyChange("title", old, title);
                }
-               return ret;
-       }
-       
-       public String getKey() {
-               return key;
        }
-       
-       public void setValue(String value) {
-               if (value == null) {
-                       value = "";
+
+       protected void onClick(ActionEvent e) {
+               // ※ カラー選択ダイアログは、Java7以降でないとアルファ値の設定はできない。
+               // Java6で実行するとアルファチャネルが消されたものになる。
+               // (設定ファイルとしては手作業では設定可能なので、とりあえず、このまま。)
+               Color selColor = JColorChooser.showDialog(ColorCell.this, title, selectedColor);
+               if (selColor != null) {
+                       setSelectedColor(selColor);
+                       if (actionListener != null) {
+                               actionListener.actionPerformed(e);
+                       }
                }
-               target.setProperty(key, value);
        }
-       
-       public String getValue() {
-               return target.getProperty(key);
+
+       private Color selectedColor;
+
+       public Color getSelectedColor() {
+               return selectedColor;
        }
 
-       public String getDisplayName() {
-               int sep = displayName.indexOf(';');
-               if (sep >= 0) {
-                       return displayName.substring(sep + 1);
+       public void setSelectedColor(Color color) {
+               if (color == null) {
+                       color = Color.BLACK;
+               }
+               Color old = this.selectedColor;
+               if (old == null ? color != null : !old.equals(color)) {
+                       this.selectedColor = color;
+
+                       Color colorForeground = new Color(color.getRGB() ^ 0xffffff).brighter();
+
+                       int alpha = color.getAlpha();
+
+                       // JPanelの背景色としてアルファの透過色をそのまま使うと
+                       // 親コンポーネントの背景色と混じり、色のカタログとして用をなさないので
+                       // 白を背景色とした合成済みに補正しておく
+                       // (アルファが255の場合はそのままで良い)
+                       Color premultipliedColor;
+                       if (alpha == 255) {
+                               premultipliedColor = color;
+                       } else {
+                               float[] rgb = color.getRGBColorComponents(null);
+                               float[] bgRgb = Color.WHITE.getRGBColorComponents(null); // 背景色 = 白色
+                               // アルファを合成済みにする
+                               float a = ((float) alpha) / 255f;
+                               rgb[0] = rgb[0] * a + bgRgb[0] * (1 - a);
+                               rgb[1] = rgb[1] * a + bgRgb[1] * (1 - a);
+                               rgb[2] = rgb[2] * a + bgRgb[2] * (1 - a);
+                               premultipliedColor = new Color(rgb[0], rgb[1], rgb[2]);
+                       }
+
+                       box.setBackground(premultipliedColor);
+                       label.setForeground(colorForeground);
+
+                       String msg;
+                       if (alpha != 255) {
+                               // アルファが255以外の場合はアルファ値も含めてARGBで表示する
+                               msg = String.format("#%08X", ((long) color.getRGB()) & 0xffffffffL);
+                       } else {
+                               // アルファが255の場合はRGBのみ表示する。
+                               msg = String.format("#%06X", ((long) color.getRGB()) & 0xffffffL);
+                       }
+                       label.setText(msg);
+
+                       firePropertyChange("selectedColor", old, color);
                }
-               return displayName;
-       }
-       
-       public boolean isRejected() {
-               return rejected;
-       }
-       
-       public void setRejected(boolean rejected) {
-               this.rejected = rejected;
        }
 }
 
-class AppConfigTableModel extends AbstractTableModelWithComboBoxModel<AppConfigRowModel> {
+/**
+ * カラーセルのレンダラー
+ */
+class ColorCellRender extends DefaultTableCellRenderer {
 
        private static final long serialVersionUID = 1L;
 
-       private static final String[] COLUMN_NAMES;
-       
-       private static final int[] COLUMN_WIDTHS;
-       
-       private Properties target = new Properties();
-       
-       private Properties original;
-       
-       static {
-               Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
-                               .getLocalizedProperties("languages/appconfigdialog");
+       private ColorCell panel = new ColorCell();
 
-               COLUMN_NAMES = new String[] {
-                               strings.getProperty("column.key"),
-                               strings.getProperty("column.value"),
-               };
-               COLUMN_WIDTHS = new int[] {
-                               Integer.parseInt(strings.getProperty("column.key.width")),
-                               Integer.parseInt(strings.getProperty("column.value.width")),
-               };
+       @Override
+       public Component getTableCellRendererComponent(JTable table, Object value,
+                       boolean isSelected, boolean hasFocus, int row, int column) {
+               panel.setSelectedColor((Color) value);
+               return panel;
        }
-       
-       public void initModel(Properties original) {
-               clear();
-               target.clear();
-
-               this.original = original;
-               if (original != null) {
-                       target.putAll(original);
-                       
-                       Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
-                                       .getLocalizedProperties("languages/appconfigdialog");
+}
 
-                       for (Object key : target.keySet()) {
-                               String displayName = strings.getProperty((String) key);
-                               if (displayName == null || displayName.length() == 0) {
-                                       displayName = (String) key;
-                               }
-                               
-                               AppConfigRowModel rowModel = new AppConfigRowModel(target, (String) key, displayName);
-                               addRow(rowModel);
-                       }
-               }
+/**
+ * カラーセルを編集モードにした場合のエディタ
+ */
+class ColorCellEditor extends AbstractCellEditor implements TableCellEditor {
 
-               sort();
-       }
-       
-       public void sort() {
-               Collections.sort(elements);
-               fireTableDataChanged();
-       }
-       
-       public void setRejectNames(Set<String> rejectNames) {
-               for (AppConfigRowModel rowModel : elements) {
-                       String key = rowModel.getKey();
-                       boolean rejected = (rejectNames != null && rejectNames.contains(key));
-                       rowModel.setRejected(rejected);
-               }
-               fireTableDataChanged();
-       }
-       
-       /**
-        * 編集されているか?
-        * 
-        * @return 編集されていればtrue、そうでなければfalse
-        */
-       public boolean isModified() {
-               if (original == null) {
-                       return true;
-               }
-               return !original.equals(target);
-       }
-       
-       public Properties getProperties() {
-               return target;
-       }
-       
-       
-       public int getColumnCount() {
-               return COLUMN_NAMES.length;
-       }
-       
-       @Override
-       public Class<?> getColumnClass(int columnIndex) {
-               return String.class;
-       }
-       
-       @Override
-       public String getColumnName(int column) {
-               return COLUMN_NAMES[column];
-       }
-       
-       @Override
-       public boolean isCellEditable(int rowIndex, int columnIndex) {
-               if (columnIndex == 1) {
-                       return true;
-               }
-               return false;
-       }
-       
-       public Object getValueAt(int rowIndex, int columnIndex) {
-               AppConfigRowModel rowModel = getRow(rowIndex);
-               switch (columnIndex) {
-               case 0:
-                       return rowModel.getDisplayName();
-               case 1:
-                       return rowModel.getValue();
-               }
-               return "";
-       }
-       
-       @Override
-       public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
-               AppConfigRowModel rowModel = getRow(rowIndex);
-               if (columnIndex == 1) {
-                       rowModel.setValue((String) aValue);
-                       fireTableCellUpdated(rowIndex, columnIndex);
+       private static final long serialVersionUID = 1L;
+
+       private ColorCell colorCell = new ColorCell(new ActionListener() {
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       fireEditingStopped();
                }
+       });
+
+       public Component getTableCellEditorComponent(final JTable table, final Object value,
+                       final boolean isSelected, final int row, final int column) {
+               colorCell.setSelectedColor((Color) value);
+               return colorCell;
        }
-       
-       public void adjustColumnModel(TableColumnModel columnModel) {
-               int mx = columnModel.getColumnCount();
-               for (int idx = 0; idx < mx; idx++) {
-                       columnModel.getColumn(idx).setWidth(COLUMN_WIDTHS[idx]);
-               }
+
+       public Object getCellEditorValue() {
+               return colorCell.getSelectedColor();
        }
 }