OSDN Git Service

AppConfig編集後のUIの色設定の反映の改善
[charactermanaj/CharacterManaJ.git] / src / main / java / charactermanaj / ui / AppConfigDialog.java
1 package charactermanaj.ui;
2
3 import java.awt.BorderLayout;
4 import java.awt.Color;
5 import java.awt.Component;
6 import java.awt.Container;
7 import java.awt.Dimension;
8 import java.awt.GridBagConstraints;
9 import java.awt.GridBagLayout;
10 import java.awt.Insets;
11 import java.awt.Toolkit;
12 import java.awt.event.ActionEvent;
13 import java.awt.event.ActionListener;
14 import java.awt.event.KeyEvent;
15 import java.awt.event.MouseEvent;
16 import java.awt.event.WindowAdapter;
17 import java.awt.event.WindowEvent;
18 import java.io.File;
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.Comparator;
22 import java.util.EnumSet;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Properties;
27 import java.util.Set;
28 import java.util.logging.Level;
29 import java.util.logging.Logger;
30
31 import javax.swing.AbstractAction;
32 import javax.swing.AbstractCellEditor;
33 import javax.swing.Action;
34 import javax.swing.ActionMap;
35 import javax.swing.BorderFactory;
36 import javax.swing.Box;
37 import javax.swing.InputMap;
38 import javax.swing.JButton;
39 import javax.swing.JCheckBox;
40 import javax.swing.JColorChooser;
41 import javax.swing.JComponent;
42 import javax.swing.JDialog;
43 import javax.swing.JFrame;
44 import javax.swing.JLabel;
45 import javax.swing.JOptionPane;
46 import javax.swing.JPanel;
47 import javax.swing.JRootPane;
48 import javax.swing.JScrollPane;
49 import javax.swing.JTable;
50 import javax.swing.KeyStroke;
51 import javax.swing.SwingConstants;
52 import javax.swing.event.TableModelEvent;
53 import javax.swing.event.TableModelListener;
54 import javax.swing.table.AbstractTableModel;
55 import javax.swing.table.DefaultTableCellRenderer;
56 import javax.swing.table.TableCellEditor;
57 import javax.swing.table.TableCellRenderer;
58 import javax.swing.table.TableColumnModel;
59
60 import charactermanaj.Main;
61 import charactermanaj.model.AppConfig;
62 import charactermanaj.util.BeanPropertiesUtilities;
63 import charactermanaj.util.BeanPropertiesUtilities.PropertyAccessor;
64 import charactermanaj.util.BeanPropertiesUtilities.PropertyAccessorMap;
65 import charactermanaj.util.ConfigurationDirUtilities;
66 import charactermanaj.util.DesktopUtilities;
67 import charactermanaj.util.ErrorMessageHelper;
68 import charactermanaj.util.LocalizedResourcePropertyLoader;
69 import charactermanaj.util.SetupLocalization;
70
71
72 /**
73  * アプリケーション設定ダイアログ
74  *
75  * @author seraphy
76  */
77 public class AppConfigDialog extends JDialog {
78
79         private static final long serialVersionUID = 1L;
80
81         private static final Logger logger = Logger.getLogger(AppConfigDialog.class.getName());
82
83         private AppConfigTableModel appConfigTableModel;
84
85         private JTable appConfigTable;
86
87         private JCheckBox chkResetDoNotAskAgain;
88
89         private RecentCharactersDir recentCharactersDir;
90
91         private AbstractAction actApply;
92
93         private boolean orgDoNotAskAgain;
94
95         public enum ColumnDef {
96                 NAME("column.key", String.class, false) {
97                         @Override
98                         public Object getValue(AppConfigRow row) {
99                                 return row.getDisplayName();
100                         }
101                 },
102                 VALUE("column.value", String.class, true) {
103                         @Override
104                         public Object getValue(AppConfigRow row) {
105                                 return row.getValue();
106                         }
107                         @Override
108                         public void setValue(AppConfigRow row, Object value) {
109                                 row.setValue(value);
110                         }
111                 };
112
113                 private final String reskey;
114
115                 private final Class<?> type;
116
117                 private final boolean editable;
118
119                 ColumnDef(String reskey, Class<?> type, boolean editable) {
120                         this.reskey = reskey;
121                         this.type = type;
122                         this.editable = editable;
123                 }
124
125                 public boolean isEditable() {
126                         return editable;
127                 }
128
129                 public String getResourceKey() {
130                         return reskey;
131                 }
132
133                 public Class<?> getType() {
134                         return type;
135                 }
136
137                 public abstract Object getValue(AppConfigRow row);
138
139                 public void setValue(AppConfigRow row, Object value) {
140                         throw new UnsupportedOperationException(name());
141                 }
142         }
143
144         private static class AppConfigRow {
145
146                 private final String name;
147
148                 private final PropertyAccessor accessor;
149
150                 private String order = "";
151
152                 private String displayName;
153
154                 private Object orgValue;
155
156                 private Object value;
157
158                 private boolean rejected;
159
160                 public AppConfigRow(String name, PropertyAccessor accessor, Object value) {
161                         this.name = name;
162                         this.accessor = accessor;
163                         this.value = value;
164                         this.orgValue = value;
165                 }
166
167                 public String getName() {
168                         return name;
169                 }
170
171                 public Class<?> getPropertyType() {
172                         Class<?> dataType = accessor.getPropertyType();
173                         // JTableのセルレンダラーではプリミティブ型の編集は対応していないので
174                         // ラッパー型に置き換える
175                         if (dataType.isPrimitive()) {
176                                 if (dataType.equals(int.class)) {
177                                         dataType = Integer.class;
178                                 } else if (dataType.equals(long.class)) {
179                                         dataType = Long.class;
180                                 } else if (dataType.equals(float.class)) {
181                                         dataType = Float.class;
182                                 } else if (dataType.equals(double.class)) {
183                                         dataType = Double.class;
184                                 } else if (dataType.equals(boolean.class)) {
185                                         dataType = Boolean.class;
186                                 }
187                         }
188                         return dataType;
189                 }
190
191                 public String getOrder() {
192                         return order;
193                 }
194
195                 public void setOrder(String order) {
196                         if (order == null) {
197                                 order = "";
198                         }
199                         this.order = order;
200                 }
201
202                 public String getDisplayName() {
203                         return (displayName == null || displayName.length() == 0) ? name : displayName;
204                 }
205
206                 public void setDisplayName(String displayName) {
207                         this.displayName = displayName;
208                 }
209
210                 public Object getValue() {
211                         return value;
212                 }
213
214                 public void setValue(Object value) {
215                         this.value = value;
216                 }
217
218                 public boolean isRejected() {
219                         return rejected;
220                 }
221
222                 public void setRejected(boolean rejected) {
223                         this.rejected = rejected;
224                 }
225
226                 public boolean isModified() {
227                         return orgValue == null ? value != null : !orgValue.equals(value);
228                 }
229         }
230
231         private static class AppConfigTableModel extends AbstractTableModel {
232
233                 private static final long serialVersionUID = 1L;
234
235                 protected static final ColumnDef[] COLUMNS = ColumnDef.values();
236
237                 private List<AppConfigRow> items = Collections.emptyList();
238
239                 public List<AppConfigRow> getItems() {
240                         return items;
241                 }
242
243                 public void setItems(List<AppConfigRow> items) {
244                         if (items == null) {
245                                 items = Collections.emptyList();
246                         }
247                         this.items = items;
248                         fireTableDataChanged();
249                 }
250
251                 public void setRejectNames(Set<String> rejectNames) {
252                         if (rejectNames == null) {
253                                 rejectNames = Collections.emptySet();
254                         }
255                         for (AppConfigRow item : items) {
256                                 String key = item.getName();
257                                 boolean rejected = rejectNames.contains(key);
258                                 item.setRejected(rejected);
259                         }
260                         fireTableDataChanged();
261                 }
262
263                 /**
264                  * 編集されているか?
265                  *
266                  * @return 編集されていればtrue、そうでなければfalse
267                  */
268                 public boolean isModified() {
269                         return false;
270                 }
271
272
273                 @Override
274                 public int getRowCount() {
275                         return items.size();
276                 }
277
278                 public int getColumnCount() {
279                         return COLUMNS.length;
280                 }
281
282                 @Override
283                 public Class<?> getColumnClass(int columnIndex) {
284                         return COLUMNS[columnIndex].getType();
285                 }
286
287                 @Override
288                 public String getColumnName(int column) {
289                         Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
290                                         .getLocalizedProperties("languages/appconfigdialog");
291                         String reskey = COLUMNS[column].getResourceKey();
292                         return strings.getProperty(reskey, reskey);
293                 }
294
295                 @Override
296                 public boolean isCellEditable(int rowIndex, int columnIndex) {
297                         return COLUMNS[columnIndex].isEditable();
298                 }
299
300                 public Object getValueAt(int rowIndex, int columnIndex) {
301                         AppConfigRow row = items.get(rowIndex);
302                         return COLUMNS[columnIndex].getValue(row);
303                 }
304
305                 @Override
306                 public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
307                         AppConfigRow row = items.get(rowIndex);
308                         COLUMNS[columnIndex].setValue(row, aValue);
309                         fireTableRowsUpdated(rowIndex, rowIndex);
310                 }
311
312                 public void adjustColumnModel(TableColumnModel columnModel) {
313                         Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
314                                         .getLocalizedProperties("languages/appconfigdialog");
315                         int mx = columnModel.getColumnCount();
316                         for (int idx = 0; idx < mx; idx++) {
317                                 String reskey = COLUMNS[idx].getResourceKey() + ".width";
318                                 int width = Integer.parseInt(strings.getProperty(reskey));
319                                 columnModel.getColumn(idx).setPreferredWidth(width);
320                         }
321                 }
322         }
323
324         public AppConfigDialog(JFrame parent) {
325                 super(parent, true);
326                 try {
327                         setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
328                         addWindowListener(new WindowAdapter() {
329                                 @Override
330                                 public void windowClosing(WindowEvent e) {
331                                         onClose();
332                                 }
333                         });
334
335                         initComponent();
336
337                         loadData();
338
339                 } catch (RuntimeException ex) {
340                         logger.log(Level.SEVERE, "appConfig construct failed.", ex);
341                         dispose();
342                         throw ex;
343                 }
344         }
345
346         private void initComponent() {
347
348                 Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
349                                 .getLocalizedProperties("languages/appconfigdialog");
350
351                 setTitle(strings.getProperty("title"));
352
353                 Container contentPane = getContentPane();
354                 contentPane.setLayout(new BorderLayout());
355
356                 // buttons
357                 JPanel btnPanel = new JPanel();
358                 btnPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 45));
359                 GridBagLayout btnPanelLayout = new GridBagLayout();
360                 btnPanel.setLayout(btnPanelLayout);
361
362                 GridBagConstraints gbc = new GridBagConstraints();
363
364                 actApply = new AbstractAction(strings.getProperty("btn.apply")) {
365                         private static final long serialVersionUID = 1L;
366                         public void actionPerformed(ActionEvent e) {
367                                 onUpdate();
368                         }
369                 };
370                 Action actCancel = new AbstractAction(strings.getProperty("btn.cancel")) {
371                         private static final long serialVersionUID = 1L;
372                         public void actionPerformed(ActionEvent e) {
373                                 onClose();
374                         }
375                 };
376                 Action actLocalization = new AbstractAction(strings.getProperty("btn.setupLocalization")) {
377                         private static final long serialVersionUID = 1L;
378                         public void actionPerformed(ActionEvent e) {
379                                 onSetupLocalization();
380                         }
381                 };
382
383                 chkResetDoNotAskAgain = new JCheckBox(strings.getProperty("chk.askForCharactersDir"));
384                 chkResetDoNotAskAgain.addActionListener(new ActionListener() {
385                         @Override
386                         public void actionPerformed(ActionEvent e) {
387                                 // 保存ボタンの状態更新のため
388                                 updateUIState();
389                         }
390                 });
391
392                 gbc.gridx = 0;
393                 gbc.gridy = 0;
394                 gbc.gridheight = 1;
395                 gbc.gridwidth = 3;
396                 gbc.anchor = GridBagConstraints.WEST;
397                 gbc.fill = GridBagConstraints.NONE;
398                 gbc.insets = new Insets(0, 0, 0, 0);
399                 gbc.ipadx = 0;
400                 gbc.ipady = 0;
401                 gbc.weightx = 1.;
402                 gbc.weighty = 0.;
403                 btnPanel.add(chkResetDoNotAskAgain, gbc);
404
405                 gbc.gridx = 0;
406                 gbc.gridy = 1;
407                 gbc.gridheight = 1;
408                 gbc.gridwidth = 3;
409                 gbc.anchor = GridBagConstraints.WEST;
410                 gbc.fill = GridBagConstraints.NONE;
411                 gbc.insets = new Insets(3, 3, 3, 3);
412                 gbc.ipadx = 0;
413                 gbc.ipady = 0;
414                 gbc.weightx = 1.;
415                 gbc.weighty = 0.;
416                 btnPanel.add(new JButton(actLocalization), gbc);
417
418                 gbc.gridx = 0;
419                 gbc.gridy = 2;
420                 gbc.gridheight = 1;
421                 gbc.gridwidth = 1;
422                 gbc.fill = GridBagConstraints.BOTH;
423                 gbc.weightx = 1.;
424                 gbc.weighty = 0.;
425                 btnPanel.add(Box.createHorizontalGlue(), gbc);
426
427                 gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 1;
428                 gbc.weightx = 0.;
429                 JButton btnApply = new JButton(actApply);
430                 btnPanel.add(btnApply, gbc);
431
432                 gbc.gridx = Main.isLinuxOrMacOSX() ? 1 : 2;
433                 gbc.weightx = 0.;
434                 JButton btnCancel = new JButton(actCancel);
435                 btnPanel.add(btnCancel, gbc);
436
437                 add(btnPanel, BorderLayout.SOUTH);
438
439                 setSize(600, 400);
440                 setLocationRelativeTo(getParent());
441
442                 // Notes
443                 JLabel lblCaution = new JLabel(strings.getProperty("caution"), JLabel.CENTER);
444                 lblCaution.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
445                 lblCaution.setForeground(Color.red);
446                 contentPane.add(lblCaution, BorderLayout.NORTH);
447
448                 // Model
449                 appConfigTableModel = new AppConfigTableModel();
450
451                 // JTable
452                 AppConfig appConfig = AppConfig.getInstance();
453                 final Color invalidBgColor = appConfig.getInvalidBgColor();
454
455                 appConfigTable = new JTable(appConfigTableModel) {
456                         private static final long serialVersionUID = 1L;
457
458                         @Override
459                         public Component prepareRenderer(TableCellRenderer renderer,
460                                         int row, int column) {
461                                 Component comp = super.prepareRenderer(renderer, row, column);
462                                 AppConfigRow configRow = appConfigTableModel.getItems().get(row);
463                                 if (configRow.isRejected()) {
464                                         // 差し戻された項目は警告色とする
465                                         comp.setBackground(invalidBgColor);
466                                 } else {
467                                         // そうでなければ標準色に戻す
468                                         if (isCellSelected(row, column)) {
469                                                 comp.setBackground(getSelectionBackground());
470                                         } else {
471                                                 comp.setBackground(getBackground());
472                                         }
473                                 }
474
475                                 if (configRow.isModified() && !configRow.isRejected()) {
476                                         // 変更行の色を警告色にする
477                                         // (ただし、rejectのものは背景色を変えているので何もしない)
478                                         comp.setForeground(invalidBgColor);
479                                 } else {
480                                         // そうでなければ標準色に戻す
481                                         comp.setForeground(getForeground());
482                                 }
483                                 return comp;
484                         }
485
486                         @Override
487                         public String getToolTipText(MouseEvent event) {
488                                 int row = rowAtPoint(event.getPoint());
489                                 int col = columnAtPoint(event.getPoint());
490                                 if (AppConfigTableModel.COLUMNS[col] == ColumnDef.NAME) {
491                                         // 最初の列の表示をツールチップとして表示させる
492                                         int modelRow = convertRowIndexToModel(row);
493                                         return appConfigTableModel.getItems().get(modelRow).getDisplayName();
494                                 }
495                                 return super.getToolTipText(event);
496                         }
497
498                         // 1つの列で複数のデータタイプの編集を可能にする
499                         // Jtable with different types of cells depending on data type
500                         // https://stackoverflow.com/questions/16970824/jtable-with-different-types-of-cells-depending-on-data-type
501
502                         private Class<?> editingClass;
503
504                         @Override
505                         public TableCellRenderer getCellRenderer(int row, int column) {
506                                 editingClass = null;
507                                 int modelColumn = convertColumnIndexToModel(column);
508                                 if (AppConfigTableModel.COLUMNS[modelColumn] == ColumnDef.VALUE) {
509                                         // VALUE列の場合
510                                         int modelRow = convertRowIndexToModel(row);
511                                         AppConfigRow rowData = appConfigTableModel.getItems().get(modelRow);
512
513                                         // 行のデータ型に対応するレンダラーを取得する
514                                         Class<?> dataType = rowData.getPropertyType();
515                                         TableCellRenderer renderer = getDefaultRenderer(dataType);
516                                         if (renderer != null) {
517                                                 return renderer;
518                                         }
519                                 }
520                                 // VALUE列以外は、標準のまま (もしくはレンダラーがみつからない場合)
521                                 return super.getCellRenderer(row, column);
522                         }
523
524                         @Override
525                         public TableCellEditor getCellEditor(int row, int column) {
526                                 editingClass = null;
527                                 int modelColumn = convertColumnIndexToModel(column);
528                                 if (AppConfigTableModel.COLUMNS[modelColumn] == ColumnDef.VALUE) {
529                                         // VALUE列の場合
530                                         int modelRow = convertRowIndexToModel(row);
531                                         AppConfigRow rowData = appConfigTableModel.getItems().get(modelRow);
532
533                                         // 行のデータ型に対応するレンダラーを取得する
534                                         editingClass = rowData.getPropertyType();
535                                         return getDefaultEditor(editingClass);
536
537                                 } else {
538                                         // VALUE列以外は、標準のまま
539                                         return super.getCellEditor(row, column);
540                                 }
541                         }
542
543                         //  This method is also invoked by the editor when the value in the editor
544                         //  component is saved in the TableModel. The class was saved when the
545                         //  editor was invoked so the proper class can be created.
546
547                         @Override
548                         public Class<?> getColumnClass(int column) {
549                                 return editingClass != null ? editingClass : super.getColumnClass(column);
550                         }
551                 };
552
553                 appConfigTable.setShowGrid(true);
554                 appConfigTable.setGridColor(appConfig.getGridColor());
555                 appConfigTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
556                 appConfigTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
557
558                 // データタイプがColorの場合のセルレンダラーとエディタを設定する
559                 appConfigTable.setDefaultRenderer(Color.class, new ColorCellRender());
560                 appConfigTable.setDefaultEditor(Color.class, new ColorCellEditor());
561
562                 appConfigTableModel.adjustColumnModel(appConfigTable.getColumnModel());
563
564                 appConfigTableModel.addTableModelListener(new TableModelListener() {
565                         @Override
566                         public void tableChanged(TableModelEvent e) {
567                                 // テーブルが変更された場合、保存ボタンの活性やReject状態の変更のため
568                                 updateUIState();
569                         }
570                 });
571
572                 JScrollPane appConfigTableSP = new JScrollPane(appConfigTable);
573                 appConfigTableSP.setBorder(BorderFactory.createCompoundBorder(
574                                 BorderFactory.createEmptyBorder(0, 3, 0, 3),
575                                 BorderFactory.createTitledBorder(strings.getProperty("table.caption")))
576                                 );
577                 appConfigTableSP.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
578                 contentPane.add(appConfigTableSP, BorderLayout.CENTER);
579
580                 // RootPane
581                 Toolkit tk = Toolkit.getDefaultToolkit();
582                 JRootPane rootPane = getRootPane();
583                 InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
584                 ActionMap am = rootPane.getActionMap();
585                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeAppConfigDialog");
586                 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "closeAppConfigDialog");
587                 am.put("closeAppConfigDialog", actCancel);
588
589                 // 保存ボタンの活性制御
590                 updateUIState();
591         }
592
593         /**
594          * 保存ボタンの活性制御とテーブルの編集状態の表示の更新
595          * (テーブルが編集された場合に更新される)
596          */
597         protected void updateUIState() {
598                 boolean hasModified = false;
599
600                 // テーブルを走査して変更を確認する
601                 for (AppConfigRow itemRow : appConfigTableModel.getItems()) {
602                         if (itemRow.isModified()) {
603                                 // 変更あり
604                                 hasModified = true;
605                         } else if (itemRow.isRejected()) {
606                                 // 変更されていない状態であれば、差し戻し状態を解除する
607                                 itemRow.setRejected(false);
608                         }
609                 }
610
611                 // キャラクターデータディレクトリの問い合わせ状態が変わっているか?
612                 if (orgDoNotAskAgain != chkResetDoNotAskAgain.isSelected()) {
613                         hasModified = true;
614                 }
615
616                 // 保存先が無効であれば適用ボタンを有効にしない.
617                 AppConfig appConfig = AppConfig.getInstance();
618                 boolean enableSave = !appConfig.getPrioritySaveFileList().isEmpty();
619
620                 // 保存が有効であり、且つ、変更された行があれば保存ボタンを有効とする
621                 actApply.setEnabled(enableSave && hasModified);
622         }
623
624         private void loadData() {
625
626                 Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
627                                 .getLocalizedProperties("languages/appconfigdialog");
628
629                 // AppConfigへのアクセッサを取得する
630                 PropertyAccessorMap accessorMap = BeanPropertiesUtilities.getPropertyAccessorMap(AppConfig.class);
631
632                 AppConfig appConfig = AppConfig.getInstance();
633                 accessorMap.setBean(appConfig);
634
635                 List<AppConfigRow> items = new ArrayList<AppConfigRow>();
636                 int fallbackOrder = 1000;
637                 for (Map.Entry<String, PropertyAccessor> accessorEntry : accessorMap.entrySet()) {
638                         // プロパティ名と現在値を取得する
639                         String name = accessorEntry.getKey();
640                         PropertyAccessor accessor = accessorEntry.getValue();
641                         Object value = accessor.getValue();
642
643                         // リソースからプロパティ名に対応する表示名を取得する(なければプロパティ名のまま)
644                         String displayName = strings.getProperty(name, name);
645                         int pt = displayName.indexOf(";");
646                         String order = Integer.toString(fallbackOrder++);
647                         if (pt > 0) {
648                                 order = displayName.substring(0, pt);
649                                 displayName = displayName.substring(pt + 1);
650                         }
651
652                         // 行オブジェクト作成
653                         AppConfigRow rowItem = new AppConfigRow(name, accessor, value);
654                         rowItem.setDisplayName(displayName);
655                         rowItem.setOrder(order);
656
657                         items.add(rowItem);
658                 }
659
660                 // 表示順に並べる
661                 Collections.sort(items, new Comparator<AppConfigRow>() {
662                         @Override
663                         public int compare(AppConfigRow o1, AppConfigRow o2) {
664                                 int ret = o1.getOrder().compareTo(o2.getOrder());
665                                 if (ret == 0) {
666                                         ret = o1.getDisplayName().compareTo(o2.getDisplayName());
667                                 }
668                                 if (ret == 0) {
669                                         ret = o1.getName().compareTo(o2.getName());
670                                 }
671                                 return ret;
672                         }
673                 });
674
675                 appConfigTableModel.setItems(items);
676
677                 // 最後に使ったキャラクターデータディレクトリの自動選択設定
678                 try {
679                         recentCharactersDir = RecentCharactersDir.load();
680
681                         if (recentCharactersDir != null) {
682                                 File lastUseCharactersDir = recentCharactersDir.getLastUseCharacterDir();
683                                 boolean enableLastUseCharacterDir = lastUseCharactersDir != null && lastUseCharactersDir.isDirectory();
684                                 boolean doNotAskAgain = enableLastUseCharacterDir && recentCharactersDir.isDoNotAskAgain();
685                                 chkResetDoNotAskAgain.setEnabled(enableLastUseCharacterDir);
686                                 chkResetDoNotAskAgain.setSelected(!doNotAskAgain);
687                         }
688
689                 } catch (Exception ex) {
690                         recentCharactersDir = null;
691                         logger.log(Level.WARNING, "RecentCharactersDir load failed.", ex);
692                 }
693
694                 // 初期状態の保存
695                 this.orgDoNotAskAgain = chkResetDoNotAskAgain.isSelected();
696         }
697
698         /**
699          * ローカライズリソースをユーザディレクトリ上に展開する.
700          */
701         protected void onSetupLocalization() {
702                 Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
703                         .getLocalizedProperties("languages/appconfigdialog");
704                 if (JOptionPane.showConfirmDialog(this,
705                                 strings.getProperty("setupLocalization"),
706                                 strings.getProperty("confirm.setupLocalization.caption"),
707                                 JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.OK_OPTION) {
708                         return;
709                 }
710
711                 try {
712                         File baseDir = ConfigurationDirUtilities.getUserDataDir();
713                         SetupLocalization setup = new SetupLocalization(baseDir);
714                         setup.setupToLocal(
715                                         EnumSet.allOf(SetupLocalization.Resources.class), true);
716
717                         File resourceDir = setup.getResourceDir();
718                         DesktopUtilities.open(resourceDir);
719
720                 } catch (Exception ex) {
721                         ErrorMessageHelper.showErrorDialog(this, ex);
722                 }
723         }
724
725         protected void onClose() {
726                 if (appConfigTableModel.isModified()) {
727                         Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
728                                         .getLocalizedProperties("languages/appconfigdialog");
729                         if (JOptionPane.showConfirmDialog(this, strings.getProperty("confirm.close"),
730                                         strings.getProperty("confirm.close.caption"), JOptionPane.YES_NO_OPTION,
731                                         JOptionPane.QUESTION_MESSAGE) != JOptionPane.YES_OPTION) {
732                                 return;
733                         }
734                 }
735                 dispose();
736         }
737
738         /**
739          * AppConfigと、キャラクターデータディレクトリの起動時の選択有無の設定値を保存する。
740          */
741         protected void onUpdate() {
742
743                 if (appConfigTable.isEditing()) {
744                         // 編集中ならば許可しない.
745                         Toolkit tk = Toolkit.getDefaultToolkit();
746                         tk.beep();
747                         return;
748                 }
749
750                 Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
751                                 .getLocalizedProperties("languages/appconfigdialog");
752
753                 // 編集されたAppConfigの設定値を取得する. (変更のあるもののみ)
754                 Map<String, Object> modifiedValues = new HashMap<String, Object>();
755                 for (AppConfigRow rowItem : appConfigTableModel.getItems()) {
756                         if (rowItem.isModified()) {
757                                 String name = rowItem.getName();
758                                 Object value = rowItem.getValue();
759                                 modifiedValues.put(name, value);
760                         }
761                 }
762
763                 // キャラクターデータディレクトリの起動時の選択状態の変更状態
764                 boolean updateRecentCharactersDir = (orgDoNotAskAgain != chkResetDoNotAskAgain.isSelected());
765
766                 if (!updateRecentCharactersDir && modifiedValues.isEmpty()) {
767                         // 変更点がないので何もしない
768                         return;
769                 }
770
771                 // AppConfigの保存
772                 if (!modifiedValues.isEmpty()) {
773                         // 編集されたプロパティが適用可能か検証する.
774                         Set<String> rejectNames = AppConfig.checkProperties(modifiedValues);
775                         if (!rejectNames.isEmpty()) {
776                                 // エラーがある場合
777                                 appConfigTableModel.setRejectNames(rejectNames);
778
779                                 JOptionPane.showMessageDialog(this, strings.getProperty("error.message"),
780                                                 strings.getProperty("error.caption"), JOptionPane.ERROR_MESSAGE);
781                                 return;
782                         }
783
784                         try {
785                                 // アプリケーション設定を更新し、保存する.
786                                 AppConfig appConfig = AppConfig.getInstance();
787                                 appConfig.update(modifiedValues);
788                                 appConfig.saveConfig();
789
790                         } catch (Exception ex) {
791                                 ErrorMessageHelper.showErrorDialog(this, ex);
792                                 return;
793                         }
794                 }
795
796                 // キャラクターデータディレクトリの起動時の選択の保存
797                 if (updateRecentCharactersDir) {
798                         try {
799                                 if (chkResetDoNotAskAgain.isEnabled()) {
800                                         boolean doNotAskAgain = !chkResetDoNotAskAgain.isSelected();
801                                         if (doNotAskAgain != recentCharactersDir.isDoNotAskAgain()) {
802                                                 recentCharactersDir.setDoNotAskAgain(doNotAskAgain);
803                                                 recentCharactersDir.saveRecents();
804                                         }
805                                 }
806                         } catch (Exception ex) {
807                                 ErrorMessageHelper.showErrorDialog(this, ex);
808                                 return;
809                         }
810                 }
811
812                 // アプリケーションの再起動が必要なことを示すダイアログを表示する.
813                 String message = strings.getProperty("caution");
814                 JOptionPane.showMessageDialog(this, message);
815
816                 dispose();
817         }
818 }
819
820 /**
821  * カラーセル
822  */
823 class ColorCell extends JPanel {
824         private static final long serialVersionUID = 1L;
825
826         private String title = "Color";
827
828         private JPanel box;
829
830         private JLabel label;
831
832         private JButton button;
833
834         private ActionListener actionListener;
835
836         public ColorCell() {
837                 this(null);
838         }
839
840         /**
841          * ボタンのアクションリスナを指定して構築する
842          * @param actionListener
843          */
844         public ColorCell(ActionListener actionListener) {
845                 super(new BorderLayout());
846                 this.actionListener = actionListener;
847
848                 box = new JPanel(new BorderLayout());
849
850                 label = new JLabel();
851                 label.setHorizontalAlignment(SwingConstants.CENTER);
852                 box.add(label, BorderLayout.CENTER);
853                 box.setBorder(BorderFactory.createEtchedBorder());
854
855                 button = new JButton();
856                 Dimension dim = button.getPreferredSize();
857                 dim.width = 24;
858                 button.setPreferredSize(dim);
859                 button.addActionListener(new ActionListener() {
860                         @Override
861                         public void actionPerformed(ActionEvent e) {
862                                 onClick(e);
863                         }
864                 });
865
866                 add(box, BorderLayout.CENTER);
867                 add(button, BorderLayout.EAST);
868                 setSelectedColor(Color.BLACK);
869         }
870
871         public String getTitle() {
872                 return title;
873         }
874
875         public void setTitle(String title) {
876                 String old = this.title;
877                 if (old == null ? title != null : !old.equals(title)) {
878                         this.title = title;
879                         firePropertyChange("title", old, title);
880                 }
881         }
882
883         protected void onClick(ActionEvent e) {
884                 // ※ カラー選択ダイアログは、Java7以降でないとアルファ値の設定はできない。
885                 // Java6で実行するとアルファチャネルが消されたものになる。
886                 // (設定ファイルとしては手作業では設定可能なので、とりあえず、このまま。)
887                 Color selColor = JColorChooser.showDialog(ColorCell.this, title, selectedColor);
888                 if (selColor != null) {
889                         setSelectedColor(selColor);
890                         if (actionListener != null) {
891                                 actionListener.actionPerformed(e);
892                         }
893                 }
894         }
895
896         private Color selectedColor;
897
898         public Color getSelectedColor() {
899                 return selectedColor;
900         }
901
902         public void setSelectedColor(Color color) {
903                 if (color == null) {
904                         color = Color.BLACK;
905                 }
906                 Color old = this.selectedColor;
907                 if (old == null ? color != null : !old.equals(color)) {
908                         this.selectedColor = color;
909
910                         Color colorForeground = new Color(color.getRGB() ^ 0xffffff).brighter();
911
912                         int alpha = color.getAlpha();
913
914                         // JPanelの背景色としてアルファの透過色をそのまま使うと
915                         // 親コンポーネントの背景色と混じり、色のカタログとして用をなさないので
916                         // 白を背景色とした合成済みに補正しておく
917                         // (アルファが255の場合はそのままで良い)
918                         Color premultipliedColor;
919                         if (alpha == 255) {
920                                 premultipliedColor = color;
921                         } else {
922                                 float[] rgb = color.getRGBColorComponents(null);
923                                 float[] bgRgb = Color.WHITE.getRGBColorComponents(null); // 背景色 = 白色
924                                 // アルファを合成済みにする
925                                 float a = ((float) alpha) / 255f;
926                                 rgb[0] = rgb[0] * a + bgRgb[0] * (1 - a);
927                                 rgb[1] = rgb[1] * a + bgRgb[1] * (1 - a);
928                                 rgb[2] = rgb[2] * a + bgRgb[2] * (1 - a);
929                                 premultipliedColor = new Color(rgb[0], rgb[1], rgb[2]);
930                         }
931
932                         box.setBackground(premultipliedColor);
933                         label.setForeground(colorForeground);
934
935                         String msg;
936                         if (alpha != 255) {
937                                 // アルファが255以外の場合はアルファ値も含めてARGBで表示する
938                                 msg = String.format("#%08X", ((long) color.getRGB()) & 0xffffffffL);
939                         } else {
940                                 // アルファが255の場合はRGBのみ表示する。
941                                 msg = String.format("#%06X", ((long) color.getRGB()) & 0xffffffL);
942                         }
943                         label.setText(msg);
944
945                         firePropertyChange("selectedColor", old, color);
946                 }
947         }
948 }
949
950 /**
951  * カラーセルのレンダラー
952  */
953 class ColorCellRender extends DefaultTableCellRenderer {
954
955         private static final long serialVersionUID = 1L;
956
957         private ColorCell panel = new ColorCell();
958
959         @Override
960         public Component getTableCellRendererComponent(JTable table, Object value,
961                         boolean isSelected, boolean hasFocus, int row, int column) {
962                 panel.setSelectedColor((Color) value);
963                 return panel;
964         }
965 }
966
967 /**
968  * カラーセルを編集モードにした場合のエディタ
969  */
970 class ColorCellEditor extends AbstractCellEditor implements TableCellEditor {
971
972         private static final long serialVersionUID = 1L;
973
974         private ColorCell colorCell = new ColorCell(new ActionListener() {
975                 @Override
976                 public void actionPerformed(ActionEvent e) {
977                         fireEditingStopped();
978                 }
979         });
980
981         public Component getTableCellEditorComponent(final JTable table, final Object value,
982                         final boolean isSelected, final int row, final int column) {
983                 colorCell.setSelectedColor((Color) value);
984                 return colorCell;
985         }
986
987         public Object getCellEditorValue() {
988                 return colorCell.getSelectedColor();
989         }
990 }