• R/O
  • SSH
  • HTTPS

charactermanaj: コミット


コミットメタ情報

リビジョン92 (tree)
日時2013-12-31 07:02:44
作者seraphy

ログメッセージ

お気に入りメニューをスクロール可能にした。
お気に入りの管理をテーブル表示とし、プリセットも合わせて管理可能にした。
プロファイル編集画面のお気に入り、プリセットの表示順を名前でソート
お気に入りメニューの階層化サポート
古いキャッシュファイルの自動削除対応
お気に入り管理ダイアログで右クリックによる選択に対応
キャラクターデータフォルダ選択時に(開始前に)ワーキングセットの全クリアを可能
ウィンドウ位置調整部の共通化、

および、お気に入り管理ダイアログをメインウィンドウの左側に設定

お気に入りの管理ダイアログをモードレスダイアログに変更。
お気に入りの変更イベント管理用のオブザーバーを独立させた。
インポート時にウェイトカーソルのまま戻らない問題の修正(おそらく)
新規インポート時に、パーツもプリセットも指定しない場合に空の構造を作成できる

変更サマリ

差分

--- trunk/src/charactermanaj/model/PartsSet.java (revision 91)
+++ trunk/src/charactermanaj/model/PartsSet.java (revision 92)
@@ -5,6 +5,7 @@
55 import java.util.AbstractMap;
66 import java.util.ArrayList;
77 import java.util.Arrays;
8+import java.util.Comparator;
89 import java.util.HashMap;
910 import java.util.List;
1011 import java.util.Map;
@@ -14,8 +15,9 @@
1415 * パーツセット.<br>
1516 * 各カテゴリの選択パーツと、そのパーツの色情報、および背景色をセットにしたもの.<br>
1617 * 保存する必要がなければIDおよび表示名は使用されないため、nullとなりえる.<br>
18+ *
1719 * @author seraphy
18- *
20+ *
1921 */
2022 public final class PartsSet extends AbstractMap<PartsCategory, List<PartsIdentifier>> implements Serializable, Cloneable {
2123
@@ -25,6 +27,23 @@
2527 private static final long serialVersionUID = 5972528889825451761L;
2628
2729 /**
30+ * PartsSet用のデフォルトのコンパレータ.<br>
31+ * 名前順、ID順にソートする.<br>
32+ */
33+ public static final Comparator<PartsSet> DEFAULT_COMPARATOR = new Comparator<PartsSet>() {
34+ public int compare(PartsSet o1, PartsSet o2) {
35+ int ret = o1.getLocalizedName().compareTo(o2.getLocalizedName());
36+ if (ret == 0) {
37+ ret = o1.getPartsSetId().compareTo(o2.getPartsSetId());
38+ }
39+ if (ret == 0) {
40+ ret = o1.hashCode() - o2.hashCode();
41+ }
42+ return ret;
43+ }
44+ };
45+
46+ /**
2847 * パーツセットID.<br>
2948 * 一時的な名前なしのパーツセットとして使われる場合はnull
3049 */
@@ -73,9 +92,13 @@
7392
7493 /**
7594 * 名前つきパーツセットを作成する.<br>
76- * @param partsSetId パーツセットID
77- * @param localizedName 表示名
78- * @param presetParts プリセットフラグ
95+ *
96+ * @param partsSetId
97+ * パーツセットID
98+ * @param localizedName
99+ * 表示名
100+ * @param presetParts
101+ * プリセットフラグ
79102 */
80103 public PartsSet(String partsSetId, String localizedName, boolean presetParts) {
81104 this.partsSetId = partsSetId;
@@ -87,8 +110,11 @@
87110 * パーツセットをディープコピーする.<br>
88111 * 現在のキャラクターデータのカテゴリインスタンスに関連づけて再生させる場合は、
89112 * resolverに現在のキャラクターデータのカテゴリリゾルバを指定します.<br>
90- * @param org 元オブジェクト
91- * @param resolver パーツセットのカテゴリを再生するためのリゾルバ、再生する必要がなければnull可
113+ *
114+ * @param org
115+ * 元オブジェクト
116+ * @param resolver
117+ * パーツセットのカテゴリを再生するためのリゾルバ、再生する必要がなければnull可
92118 */
93119 protected PartsSet(PartsSet org, PartsCategoryResolver resolver) {
94120 if (org == null) {
@@ -149,7 +175,9 @@
149175 * パーツセットはキャラクターデータとは独立してロード・セーブされることがあるため、同じ情報でも異なるインスタンスとなる場合があり、
150176 * これを是正するために、このメソッドを使用します.<br>
151177 * リゾルバから取得できないパーツカテゴリは除去されます.<br>
152- * @param resolver リゾルバ
178+ *
179+ * @param resolver
180+ * リゾルバ
153181 * @return 互換性のあるパーツセット
154182 */
155183 public PartsSet createCompatible(PartsCategoryResolver resolver) {
@@ -162,6 +190,7 @@
162190
163191 /**
164192 * パーツセットをディープコピーする.<br>
193+ *
165194 * @return コピーされたパーツセット
166195 */
167196 @Override
@@ -208,8 +237,8 @@
208237 }
209238
210239 /**
211- * プリセット用パーツセットであるか?
212- * これがfalseの場合は一時的なパーツセットか、もしくはお気に入り用である.<br>
240+ * プリセット用パーツセットであるか? これがfalseの場合は一時的なパーツセットか、もしくはお気に入り用である.<br>
241+ *
213242 * @return プリセット用パーツセットである場合
214243 */
215244 public boolean isPresetParts() {
@@ -223,6 +252,7 @@
223252 /**
224253 * バックグラウンドカラーを取得する.<br>
225254 * 設定されていなければnull.<br>
255+ *
226256 * @return バックグラウンドカラー、もしくはnull
227257 */
228258 public Color getBgColor() {
@@ -233,7 +263,9 @@
233263 * アフィン変換用パラメータを指定する.<br>
234264 * 配列は4または6でなければならない.<br>
235265 * アフィン変換しない場合はnull
236- * @param affineTransformParameter 変換パラメータ(4または6個の要素)、もしくはnull
266+ *
267+ * @param affineTransformParameter
268+ * 変換パラメータ(4または6個の要素)、もしくはnull
237269 */
238270 public void setAffineTransformParameter(double[] affineTransformParameter) {
239271 if (affineTransformParameter != null && !(affineTransformParameter.length == 4 || affineTransformParameter.length == 6)) {
@@ -245,6 +277,7 @@
245277 /**
246278 * アフィン変換用のパラメータを取得する.<br>
247279 * 変換しない場合はnull.<br>
280+ *
248281 * @return アフィン変換用のパラメータ、またはnull
249282 */
250283 public double[] getAffineTransformParameter() {
@@ -262,6 +295,7 @@
262295 /**
263296 * プリセットIDを取得する.<br>
264297 * 一時的なパーツセットである場合はnull
298+ *
265299 * @return プリセットID、またはnull
266300 */
267301 public String getPartsSetId() {
@@ -271,6 +305,7 @@
271305 /**
272306 * プリセット名を取得する.<br>
273307 * 一時的なパーツセットである場合はnull
308+ *
274309 * @return プリセット名、またはnull
275310 */
276311 public String getLocalizedName() {
@@ -288,7 +323,9 @@
288323 /**
289324 * パーツカラー情報を取得する.<br>
290325 * カラー情報が関連づけられていない場合はnullが返される.<br>
291- * @param partsIdentifier パーツ識別子
326+ *
327+ * @param partsIdentifier
328+ * パーツ識別子
292329 * @return カラー情報、もしくはnull
293330 */
294331 public PartsColorInfo getColorInfo(PartsIdentifier partsIdentifier) {
@@ -301,9 +338,13 @@
301338 * partsNameがnullまたは空文字の場合はカテゴリのみ登録する.<br>
302339 * これにより、そのカテゴリに選択がないことを示す.<br>
303340 * 複数選択可能なカテゴリの場合、複数回の呼び出しで登録する.(登録順)<br>
304- * @param category カテゴリ
305- * @param partsIdentifier パーツ識別子、またはnull
306- * @param partsColorInfo パーツの色情報、なければnull可
341+ *
342+ * @param category
343+ * カテゴリ
344+ * @param partsIdentifier
345+ * パーツ識別子、またはnull
346+ * @param partsColorInfo
347+ * パーツの色情報、なければnull可
307348 */
308349 public void appendParts(PartsCategory category, PartsIdentifier partsIdentifier, PartsColorInfo partsColorInfo) {
309350 if (category == null) {
@@ -332,7 +373,9 @@
332373 /**
333374 * パーツセットが構造的に一致するか検証します.<br>
334375 * nullの場合は常にfalseとなります.<br>
335- * @param other 比較対象、null可
376+ *
377+ * @param other
378+ * 比較対象、null可
336379 * @return パーツ構成が一致すればtrue、そうでなければfalse
337380 */
338381 public boolean isSameStructure(PartsSet other) {
@@ -356,8 +399,11 @@
356399 * 2つのパーツセットが構造的に一致するか検証します.<br>
357400 * いずれか一方がnullであればfalseを返します.双方がnullであればtrueを返します.<br>
358401 * 双方がnullでなければ{@link #isSameStructure(PartsSet)}で判定されます.<br>
359- * @param a 比較対象1、null可
360- * @param b 比較対象2、null可
402+ *
403+ * @param a
404+ * 比較対象1、null可
405+ * @param b
406+ * 比較対象2、null可
361407 * @return パーツ構成が一致すればtrue、そうでなければfalse
362408 */
363409 public static boolean isSameStructure(PartsSet a, PartsSet b) {
@@ -372,11 +418,12 @@
372418
373419 /**
374420 * 保持しているパーツ識別子のカラー情報と同一のカラー情報をもっているか判定します.<br>
375- * 相手側はカテゴリや順序を問わず、少なくとも自分と同じパーツ識別子をもっていれば足りるため、
376- * パーツ構成が同一であるかの判定は行いません.<br>
421+ * 相手側はカテゴリや順序を問わず、少なくとも自分と同じパーツ識別子をもっていれば足りるため、 パーツ構成が同一であるかの判定は行いません.<br>
377422 * パーツ構造を含めて判定を行う場合は事前に{@link #isSameStructure(PartsSet)}を呼び出します.<br>
378423 * nullの場合は常にfalseとなります.<br>
379- * @param other 判定先、null可
424+ *
425+ * @param other
426+ * 判定先、null可
380427 * @return 同一であればtrue、そうでなければfalse
381428 */
382429 public boolean isSameColor(PartsSet other) {
@@ -397,13 +444,15 @@
397444
398445 /**
399446 * 引数aが保持しているパーツ識別子のカラー情報と同一のカラー情報を引数bがもっているか判定します.<br>
400- * 引数b側はカテゴリや順序を問わず、少なくとも引数aと同じパーツ識別子をもっていれば足りるため、
401- * パーツ構成が同一であるかの判定は行いません.<br>
447+ * 引数b側はカテゴリや順序を問わず、少なくとも引数aと同じパーツ識別子をもっていれば足りるため、 パーツ構成が同一であるかの判定は行いません.<br>
402448 * パーツ構造を含めて判定を行う場合は事前に{@link #isSameStructure(PartsSet, PartsSet)}を呼び出します.<br>
403449 * 双方がnullであればtrueとなります.<br>
404450 * いずれか一方がnullの場合はfalseとなります.<br>
405- * @param a 対象1、null可
406- * @param b 対象2、null可
451+ *
452+ * @param a
453+ * 対象1、null可
454+ * @param b
455+ * 対象2、null可
407456 * @return 同一であればtrue、そうでなければfalse
408457 */
409458 public static boolean isSameColor(PartsSet a, PartsSet b) {
@@ -418,6 +467,7 @@
418467
419468 /**
420469 * このパーツセットが名前をもっているか?
470+ *
421471 * @return 名前がある場合はtrue、設定されていないか空文字の場合はfalse
422472 */
423473 public boolean hasName() {
--- trunk/src/charactermanaj/model/io/CharacterDataDefaultProvider.java (revision 91)
+++ trunk/src/charactermanaj/model/io/CharacterDataDefaultProvider.java (revision 92)
@@ -189,21 +189,22 @@
189189 return pathname.isFile() && name.endsWith(".xml");
190190 }
191191 });
192+ if (files == null) {
193+ files = new File[0];
194+ }
192195 CharacterDataPersistent persist = CharacterDataPersistent
193196 .getInstance();
194- if (files != null) {
195- for (File file : files) {
196- try {
197- URI docBase = file.toURI();
198- CharacterData cd = persist.loadProfile(docBase);
199- if (cd != null && cd.isValid()) {
200- String name = file.getName();
201- templateNameMap.put(name, cd.getName());
202- }
203- } catch (IOException ex) {
204- logger.log(Level.WARNING,
205- "failed to read templatedir." + file, ex);
197+ for (File file : files) {
198+ try {
199+ URI docBase = file.toURI();
200+ CharacterData cd = persist.loadProfile(docBase);
201+ if (cd != null && cd.isValid()) {
202+ String name = file.getName();
203+ templateNameMap.put(name, cd.getName());
206204 }
205+ } catch (IOException ex) {
206+ logger.log(Level.WARNING, "failed to read templatedir."
207+ + file, ex);
207208 }
208209 }
209210 }
--- trunk/src/charactermanaj/model/io/PartsImageDirectoryWatchAgentThread.java (revision 91)
+++ trunk/src/charactermanaj/model/io/PartsImageDirectoryWatchAgentThread.java (revision 92)
@@ -153,6 +153,7 @@
153153
154154 /**
155155 * 監視を停止する.<br>
156+ *
156157 * @return 停止した場合はtrue、すでに停止していたか開始されていない場合はfalse
157158 */
158159 private boolean stop() {
@@ -214,8 +215,11 @@
214215 /**
215216 * 監視を行う.<br>
216217 * 停止フラグが設定されるか、割り込みされた場合は処理を中断してInterruptedException例外を返して終了する.<br>
217- * @param notifier 通知するためのオブジェクト
218- * @throws InterruptedException 割り込みされた場合
218+ *
219+ * @param notifier
220+ * 通知するためのオブジェクト
221+ * @throws InterruptedException
222+ * 割り込みされた場合
219223 */
220224 protected void watch(Runnable notifier) throws InterruptedException {
221225 if (baseDir == null || !baseDir.exists() || !baseDir.isDirectory()) {
@@ -232,11 +236,15 @@
232236 File watchDir = new File(baseDir, layer.getDir());
233237 ArrayList<String> files = new ArrayList<String>();
234238 if (watchDir.exists() && watchDir.isDirectory()) {
235- for (File file : watchDir.listFiles(new FileFilter() {
239+ File[] list = watchDir.listFiles(new FileFilter() {
236240 public boolean accept(File pathname) {
237241 return pathname.isFile() && pathname.getName().toLowerCase().endsWith(".png");
238242 }
239- })) {
243+ });
244+ if (list == null) {
245+ list = new File[0];
246+ }
247+ for (File file : list) {
240248 if (Thread.interrupted() || stopFlag) {
241249 throw new InterruptedException();
242250 }
@@ -278,7 +286,9 @@
278286
279287 /**
280288 * イベントリスナを登録する
281- * @param l リスナ
289+ *
290+ * @param l
291+ * リスナ
282292 */
283293 public void addPartsImageDirectoryWatchListener(PartsImageDirectoryWatchListener l) {
284294 if (l != null) {
@@ -291,7 +301,9 @@
291301
292302 /**
293303 * イベントリスナを登録解除する
294- * @param l リスナ
304+ *
305+ * @param l
306+ * リスナ
295307 */
296308 public void removePartsImageDirectoryWatchListener(PartsImageDirectoryWatchListener l) {
297309 if (l != null) {
--- trunk/src/charactermanaj/model/io/WorkingSetPersist.java (nonexistent)
+++ trunk/src/charactermanaj/model/io/WorkingSetPersist.java (revision 92)
@@ -0,0 +1,150 @@
1+package charactermanaj.model.io;
2+
3+import java.io.File;
4+import java.io.FileFilter;
5+import java.io.IOException;
6+import java.io.InputStream;
7+import java.io.OutputStream;
8+import java.util.logging.Level;
9+import java.util.logging.Logger;
10+
11+import charactermanaj.model.CharacterData;
12+import charactermanaj.model.WorkingSet;
13+import charactermanaj.model.WorkingSet2;
14+import charactermanaj.util.UserData;
15+import charactermanaj.util.UserDataFactory;
16+
17+/**
18+ * ワーキングセットの保存と復元.<br>
19+ *
20+ * @author seraphy
21+ */
22+public class WorkingSetPersist {
23+
24+ /**
25+ * ロガー
26+ */
27+ private static final Logger logger = Logger
28+ .getLogger(WorkingSetPersist.class.getName());
29+
30+ /**
31+ * ワーキングセットのサフィックス.
32+ */
33+ public static final String WORKINGSET_FILE_SUFFIX = "workingset.xml";
34+
35+ private static final WorkingSetPersist singletion = new WorkingSetPersist();
36+
37+ public static WorkingSetPersist getInstance() {
38+ return singletion;
39+ }
40+
41+ /**
42+ * すべてのワーキングセットをクリアする.<br>
43+ */
44+ public void removeAllWorkingSet() {
45+ UserDataFactory userDataFactory = UserDataFactory.getInstance();
46+ File dir = userDataFactory.getSpecialDataDir("foo-"
47+ + WORKINGSET_FILE_SUFFIX);
48+ if (dir.exists() && dir.isDirectory()) {
49+ File[] files = dir.listFiles(new FileFilter() {
50+ public boolean accept(File pathname) {
51+ return pathname.isFile()
52+ && pathname.getName().endsWith(
53+ WORKINGSET_FILE_SUFFIX);
54+ }
55+ });
56+ if (files == null) {
57+ logger.log(Level.WARNING, "dir access failed. " + dir);
58+ return;
59+ }
60+ for (File file : files) {
61+ boolean success = file.delete();
62+ logger.log(Level.INFO, "remove file: " + file + " /success="
63+ + success);
64+ }
65+ }
66+ }
67+
68+ /**
69+ * ワーキングセットを削除する.
70+ *
71+ * @param cd
72+ * 対象のキャラクターデータ
73+ */
74+ public void removeWorkingSet(CharacterData cd) {
75+ UserDataFactory userDataFactory = UserDataFactory.getInstance();
76+ UserData workingSetXmlData = userDataFactory.getMangledNamedUserData(
77+ cd.getDocBase(), WORKINGSET_FILE_SUFFIX);
78+ if (workingSetXmlData != null && workingSetXmlData.exists()) {
79+ logger.log(Level.INFO, "remove file: " + workingSetXmlData);
80+ workingSetXmlData.delete();
81+ }
82+ }
83+
84+ /**
85+ * ワーキングセットを保存する.<br>
86+ * ワーキングセットインスタンスには、あらかじめ全て設定しておく必要がある.<br>
87+ *
88+ * @param workingSet
89+ * ワーキングセット
90+ * @throws IOException
91+ * 失敗
92+ */
93+ public void saveWorkingSet(WorkingSet workingSet) throws IOException {
94+ if (workingSet == null) {
95+ throw new IllegalArgumentException();
96+ }
97+ CharacterData characterData = workingSet.getCharacterData();
98+ if (characterData == null) {
99+ throw new IllegalArgumentException("character-data must be set.");
100+ }
101+
102+ // XML形式でのワーキングセットの保存
103+ UserDataFactory userDataFactory = UserDataFactory.getInstance();
104+ UserData workingSetXmlData = userDataFactory.getMangledNamedUserData(
105+ characterData.getDocBase(), WORKINGSET_FILE_SUFFIX);
106+ OutputStream outstm = workingSetXmlData.getOutputStream();
107+ try {
108+ WorkingSetXMLWriter workingSetXmlWriter = new WorkingSetXMLWriter();
109+ workingSetXmlWriter.writeWorkingSet(workingSet, outstm);
110+ } finally {
111+ outstm.close();
112+ }
113+ }
114+
115+ /**
116+ * ワーキングセットを取得する.<br>
117+ *
118+ * @param characterData
119+ * 対象のキャラクターデータ
120+ * @return ワーキングセット、なければnull
121+ * @throws IOException
122+ * 読み込みに失敗した場合
123+ */
124+ public WorkingSet2 loadWorkingSet(CharacterData characterData)
125+ throws IOException {
126+ if (characterData == null) {
127+ throw new IllegalArgumentException();
128+ }
129+ // XML形式でのワーキングセットの復元
130+ UserDataFactory userDataFactory = UserDataFactory.getInstance();
131+ UserData workingSetXmlData = userDataFactory.getMangledNamedUserData(
132+ characterData.getDocBase(), WORKINGSET_FILE_SUFFIX);
133+ if (workingSetXmlData == null || !workingSetXmlData.exists()) {
134+ // 保存されていない場合
135+ return null;
136+ }
137+ WorkingSet2 workingSet2;
138+
139+ InputStream is = workingSetXmlData.openStream();
140+ try {
141+ WorkingSetXMLReader WorkingSetXMLReader = new WorkingSetXMLReader();
142+ workingSet2 = WorkingSetXMLReader.loadWorkingSet(is);
143+
144+ } finally {
145+ is.close();
146+ }
147+
148+ return workingSet2;
149+ }
150+}
--- trunk/src/charactermanaj/model/io/CharacterDataPersistent.java (revision 91)
+++ trunk/src/charactermanaj/model/io/CharacterDataPersistent.java (revision 92)
@@ -41,12 +41,10 @@
4141 import charactermanaj.model.Layer;
4242 import charactermanaj.model.PartsCategory;
4343 import charactermanaj.model.io.CharacterDataDefaultProvider.DefaultCharacterDataVersion;
44-import charactermanaj.ui.MainFrame;
4544 import charactermanaj.util.DirectoryConfig;
4645 import charactermanaj.util.FileNameNormalizer;
4746 import charactermanaj.util.FileUserData;
4847 import charactermanaj.util.UserData;
49-import charactermanaj.util.UserDataFactory;
5048
5149 public class CharacterDataPersistent {
5250
@@ -352,7 +350,11 @@
352350 return;
353351 }
354352
355- for (File dir : dataDir.listFiles()) {
353+ File[] dirs = dataDir.listFiles();
354+ if (dirs == null) {
355+ dirs = new File[0];
356+ }
357+ for (File dir : dirs) {
356358 if (!dir.isDirectory()) {
357359 continue;
358360 }
@@ -439,7 +441,7 @@
439441 .newFixedThreadPool(numOfProcessors);
440442 try {
441443 // キャラクターデータ対象ディレクトリを列挙し、並列に解析する
442- for (File dir : baseDir.listFiles(new FileFilter() {
444+ File[] dirs = baseDir.listFiles(new FileFilter() {
443445 public boolean accept(File pathname) {
444446 boolean accept = pathname.isDirectory()
445447 && !pathname.getName().startsWith(".");
@@ -449,7 +451,11 @@
449451 }
450452 return accept;
451453 }
452- })) {
454+ });
455+ if (dirs == null) {
456+ dirs = new File[0];
457+ }
458+ for (File dir : dirs) {
453459 String path = normalizer.normalize(dir.getPath());
454460 final File normDir = new File(path);
455461
@@ -721,13 +727,9 @@
721727 }
722728
723729 // ワーキングセットの削除
724- UserDataFactory userDataFactory = UserDataFactory.getInstance();
725- UserData workingSetXmlData = userDataFactory.getMangledNamedUserData(
726- cd.getDocBase(), MainFrame.WORKINGSET_FILE_SUFFIX);
727- if (workingSetXmlData != null && workingSetXmlData.exists()) {
728- logger.log(Level.INFO, "remove file: " + workingSetXmlData);
729- workingSetXmlData.delete();
730- }
730+ // XML形式でのワーキングセットの保存
731+ WorkingSetPersist workingSetPersist = WorkingSetPersist.getInstance();
732+ workingSetPersist.removeWorkingSet(cd);
731733
732734 // xmlファイルの拡張子を変更することでキャラクター定義として認識させない.
733735 // (削除に失敗するケースに備えて先にリネームする.)
@@ -771,8 +773,11 @@
771773 return;
772774 }
773775 if (file.isDirectory()) {
774- for (File child : file.listFiles()) {
775- removeRecursive(child);
776+ File[] children = file.listFiles();
777+ if (children != null) {
778+ for (File child : children) {
779+ removeRecursive(child);
780+ }
776781 }
777782 }
778783 if (!file.delete()) {
--- trunk/src/charactermanaj/model/io/FilePartsDataLoader.java (revision 91)
+++ trunk/src/charactermanaj/model/io/FilePartsDataLoader.java (revision 92)
@@ -15,8 +15,9 @@
1515
1616 /**
1717 * ディレクトリを指定して、そこからキャラクターのパーツデータをロードするローダー.<br>
18+ *
1819 * @author seraphy
19- *
20+ *
2021 */
2122 public class FilePartsDataLoader implements PartsDataLoader {
2223
@@ -49,7 +50,7 @@
4950 if (!searchDir.exists() || !searchDir.isDirectory()) {
5051 continue;
5152 }
52- for (File imgFile : searchDir.listFiles(new FileFilter() {
53+ File[] imgFiles = searchDir.listFiles(new FileFilter() {
5354 public boolean accept(File pathname) {
5455 if (pathname.isFile()) {
5556 String lcfname = pathname.getName().toLowerCase();
@@ -57,7 +58,11 @@
5758 }
5859 return false;
5960 }
60- })) {
61+ });
62+ if (imgFiles == null) {
63+ imgFiles = new File[0];
64+ }
65+ for (File imgFile : imgFiles) {
6166 String partsName = normalizer.normalize(imgFile.getName());
6267
6368 int extpos = partsName.lastIndexOf(".");
--- trunk/src/charactermanaj/model/io/CharacterDataDirectoryFile.java (revision 91)
+++ trunk/src/charactermanaj/model/io/CharacterDataDirectoryFile.java (revision 92)
@@ -15,6 +15,7 @@
1515
1616 /**
1717 * ディレクトリをアーカイブと見立てる
18+ *
1819 * @author seraphy
1920 */
2021 public class CharacterDataDirectoryFile extends AbstractCharacterDataArchiveFile {
@@ -68,9 +69,12 @@
6869
6970 /**
7071 * アーカイブファイルをベースとしたURIを返す.<br>
71- * @param name コンテンツ名
72+ *
73+ * @param name
74+ * コンテンツ名
7275 * @return URI
73- * @throws IOException URIを生成できない場合
76+ * @throws IOException
77+ * URIを生成できない場合
7478 */
7579 protected URI getContentURI(String name) throws IOException {
7680 return new File(baseDir, name).toURI();
@@ -89,9 +93,10 @@
8993 }
9094
9195 /**
92- * このディレクトリに対してターゲットプロファイルのディレクトリがかぶっているか?
93- * つまり、ターゲット自身のディレクトリをソースとしていないか?
94- * @param characterData ソースプロファイル
96+ * このディレクトリに対してターゲットプロファイルのディレクトリがかぶっているか? つまり、ターゲット自身のディレクトリをソースとしていないか?
97+ *
98+ * @param characterData
99+ * ソースプロファイル
95100 * @return 被っている場合はtrue、被っていない場合はfalse
96101 */
97102 protected boolean isOverlapped(CharacterData characterData) {
@@ -134,14 +139,17 @@
134139 // ファイル名をノーマライズする
135140 FileNameNormalizer normalizer = FileNameNormalizer.getDefault();
136141
137- for (File file : dir.listFiles()) {
138- String name = normalizer.normalize(file.getName());
139- String entryName = prefix + name;
140- if (file.isDirectory()) {
141- // エントリ名の区切り文字は常に「/」とする. (ZIP/JARのentry互換のため)
142- load(file, entryName + "/");
143- } else {
144- addEntry(new DirFileContent(file, entryName));
142+ File[] files = dir.listFiles();
143+ if (files != null) {
144+ for (File file : files) {
145+ String name = normalizer.normalize(file.getName());
146+ String entryName = prefix + name;
147+ if (file.isDirectory()) {
148+ // エントリ名の区切り文字は常に「/」とする. (ZIP/JARのentry互換のため)
149+ load(file, entryName + "/");
150+ } else {
151+ addEntry(new DirFileContent(file, entryName));
152+ }
145153 }
146154 }
147155 }
--- trunk/src/charactermanaj/model/util/StartupSupport.java (revision 91)
+++ trunk/src/charactermanaj/model/util/StartupSupport.java (revision 92)
@@ -9,8 +9,8 @@
99
1010 import charactermanaj.model.AppConfig;
1111 import charactermanaj.model.WorkingSet;
12+import charactermanaj.model.io.WorkingSetPersist;
1213 import charactermanaj.model.io.WorkingSetXMLWriter;
13-import charactermanaj.ui.MainFrame;
1414 import charactermanaj.ui.RecentCharactersDir;
1515 import charactermanaj.util.FileUserData;
1616 import charactermanaj.util.UserData;
@@ -42,6 +42,7 @@
4242 new PurgeOldLogs(),
4343 new ConvertRecentCharDirsSerToXmlProps(),
4444 new ConvertWorkingSetSerToXml(),
45+ new PurgeOldCaches(),
4546 };
4647 for (StartupSupport startup : startups) {
4748 logger.log(Level.FINE, "startup operation start. class="
@@ -119,11 +120,13 @@
119120
120121 @Override
121122 public void doStartup() {
122- // 旧形式(ver0.991以前)
123- UserDataFactory userDataFactory = UserDataFactory.getInstance();
124123 final String FILENAME = "workingset.ser";
125- File dir = userDataFactory.getSpecialDataDir(FILENAME);
126124 try {
125+ UserDataFactory userDataFactory = UserDataFactory.getInstance();
126+ File dir = userDataFactory.getSpecialDataDir(FILENAME);
127+ if (!dir.exists()) {
128+ return;
129+ }
127130 File[] files = dir.listFiles(new FileFilter() {
128131 public boolean accept(File pathname) {
129132 String name = pathname.getName();
@@ -130,40 +133,41 @@
130133 return name.endsWith(FILENAME);
131134 }
132135 });
133- if (files != null) {
134- WorkingSetXMLWriter wr = new WorkingSetXMLWriter();
135- for (File file : files) {
136- FileUserData fileData = new FileUserData(file);
137- if (fileData.exists()) {
138- try {
139- // serファイルをデシリアライズする.
140- WorkingSet ws = (WorkingSet) fileData.load();
141- URI docBase = ws.getCharacterDocBase();
142- if (docBase != null) {
143- // XML形式で保存しなおす.
144- UserData workingSetXmlData = userDataFactory
145- .getMangledNamedUserData(
146- docBase,
147- MainFrame.WORKINGSET_FILE_SUFFIX);
148- if (!workingSetXmlData.exists()) {
149- // XML形式データがまだない場合のみ保存しなおす.
150- OutputStream outstm = workingSetXmlData
151- .getOutputStream();
152- try {
153- wr.writeWorkingSet(ws, outstm);
154- } finally {
155- outstm.close();
156- }
136+ if (files == null) {
137+ logger.log(Level.WARNING, "cache-dir access failed. " + dir);
138+ return;
139+ }
140+ WorkingSetXMLWriter wr = new WorkingSetXMLWriter();
141+ for (File file : files) {
142+ FileUserData fileData = new FileUserData(file);
143+ if (fileData.exists()) {
144+ try {
145+ // serファイルをデシリアライズする.
146+ WorkingSet ws = (WorkingSet) fileData.load();
147+ URI docBase = ws.getCharacterDocBase();
148+ if (docBase != null) {
149+ // XML形式で保存しなおす.
150+ UserData workingSetXmlData = userDataFactory
151+ .getMangledNamedUserData(docBase,
152+ WorkingSetPersist.WORKINGSET_FILE_SUFFIX);
153+ if (!workingSetXmlData.exists()) {
154+ // XML形式データがまだない場合のみ保存しなおす.
155+ OutputStream outstm = workingSetXmlData
156+ .getOutputStream();
157+ try {
158+ wr.writeWorkingSet(ws, outstm);
159+ } finally {
160+ outstm.close();
157161 }
158162 }
163+ }
159164
160- // serファイルは削除する.
161- fileData.delete();
165+ // serファイルは削除する.
166+ fileData.delete();
162167
163- } catch (Exception ex) {
164- logger.log(Level.WARNING, FILENAME
165- + " convert failed.", ex);
166- }
168+ } catch (Exception ex) {
169+ logger.log(Level.WARNING,
170+ FILENAME + " convert failed.", ex);
167171 }
168172 }
169173 }
@@ -194,13 +198,21 @@
194198 AppConfig appConfig = AppConfig.getInstance();
195199 long purgeOldLogsMillSec = appConfig.getPurgeLogDays() * 24L * 3600L * 1000L;
196200 if (purgeOldLogsMillSec > 0) {
197- long purgeThresold = System.currentTimeMillis() - purgeOldLogsMillSec;
198- for (File file : logsDir.listFiles()) {
201+ File[] files = logsDir.listFiles();
202+ if (files == null) {
203+ logger.log(Level.WARNING, "log-dir access failed.");
204+ return;
205+ }
206+ long purgeThresold = System.currentTimeMillis()
207+ - purgeOldLogsMillSec;
208+ for (File file : files) {
199209 try {
200210 String name = file.getName();
201- if (file.isFile() && file.canWrite() && name.endsWith(".log")) {
211+ if (file.isFile() && file.canWrite()
212+ && name.endsWith(".log")) {
202213 long lastModified = file.lastModified();
203- if (lastModified > 0 && lastModified < purgeThresold) {
214+ if (lastModified > 0
215+ && lastModified < purgeThresold) {
204216 boolean result = file.delete();
205217 logger.log(Level.INFO, "remove file " + file
206218 + "/succeeded=" + result);
@@ -208,7 +220,8 @@
208220 }
209221
210222 } catch (Exception ex) {
211- logger.log(Level.WARNING, "remove file failed. " + file, ex);
223+ logger.log(Level.WARNING,
224+ "remove file failed. " + file, ex);
212225 }
213226 }
214227 }
@@ -215,3 +228,53 @@
215228 }
216229 }
217230 }
231+
232+/**
233+ * 古いキャッシュファイルを消去する.<br>
234+ * -character.xml-cache.ser, -favorites.serは、直接xmlでの読み込みになったため、 ただちに消去しても問題ない.<br>
235+ * recent-character.serは、使用されなくなったため、ただちに消去して良い.<br>
236+ * mangled_info.xmlは、*.serを消去したあとには不要となるため、消去する.<br>
237+ * (今後使われることはない)
238+ *
239+ * @author seraphy
240+ */
241+class PurgeOldCaches extends StartupSupport {
242+
243+ /**
244+ * ロガー
245+ */
246+ private final Logger logger = Logger.getLogger(getClass().getName());
247+
248+ @Override
249+ public void doStartup() {
250+ UserDataFactory userDataFactory = UserDataFactory.getInstance();
251+ File cacheDir = userDataFactory.getSpecialDataDir(".ser");
252+ if (cacheDir.exists()) {
253+ File[] files = cacheDir.listFiles();
254+ if (files == null) {
255+ logger.log(Level.WARNING, "cache-dir access failed.");
256+ return;
257+ }
258+ for (File file : files) {
259+ try {
260+ if (!file.isFile() || !file.canWrite()) {
261+ // ファイルでないか、書き込み不可の場合はスキップする.
262+ continue;
263+ }
264+ String name = file.getName();
265+ if (name.endsWith("-character.xml-cache.ser")
266+ || name.endsWith("-favorites.ser")
267+ || name.equals("recent-character.ser")
268+ || name.equals("mangled_info.xml")) {
269+ boolean result = file.delete();
270+ logger.log(Level.INFO, "remove file " + file
271+ + "/succeeded=" + result);
272+ }
273+
274+ } catch (Exception ex) {
275+ logger.log(Level.WARNING, "remove file failed. " + file, ex);
276+ }
277+ }
278+ }
279+ }
280+}
--- trunk/src/charactermanaj/ui/SearchPartsDialog.java (revision 91)
+++ trunk/src/charactermanaj/ui/SearchPartsDialog.java (revision 92)
@@ -2,13 +2,9 @@
22
33 import java.awt.BorderLayout;
44 import java.awt.Container;
5-import java.awt.Dimension;
6-import java.awt.GraphicsEnvironment;
75 import java.awt.GridBagConstraints;
86 import java.awt.GridBagLayout;
97 import java.awt.Insets;
10-import java.awt.Point;
11-import java.awt.Rectangle;
128 import java.awt.Toolkit;
139 import java.awt.event.ActionEvent;
1410 import java.awt.event.ActionListener;
@@ -23,8 +19,8 @@
2319 import java.util.HashSet;
2420 import java.util.List;
2521 import java.util.Map;
22+import java.util.Map.Entry;
2623 import java.util.Properties;
27-import java.util.Map.Entry;
2824 import java.util.WeakHashMap;
2925
3026 import javax.swing.AbstractAction;
@@ -342,39 +338,6 @@
342338 }
343339
344340 /**
345- * ダイアログの表示位置を調整する.<br>
346- * 横位置Xはメインフレームの右側とし、縦位置Yはメインフレームの上位置からのoffset_yを加えた位置とする.<br>
347- * @param offset_y オフセットY
348- */
349- public void adjustLocation(int offset_y) {
350- // メインウィンドウよりも左側に位置づけする.
351- // 縦位置はメインウィンドウの上端からオフセットを加えたものとする.
352- Point pt = getParent().getLocation();
353- Insets insets = getParent().getInsets();
354- pt.x += getParent().getWidth();
355- pt.y += (offset_y * insets.top);
356-
357- // メインスクリーンサイズを取得する.
358- GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
359- Rectangle desktopSize = genv.getMaximumWindowBounds(); // メインスクリーンのサイズ(デスクトップ領域のみ)
360-
361- // メインスクリーンサイズを超えた場合は、はみ出た分を移動する.
362- if ((pt.x + getWidth()) > desktopSize.width) {
363- pt.x -= ((pt.x + getWidth()) - desktopSize.width);
364- }
365- if ((pt.y + getHeight()) > desktopSize.height) {
366- pt.y -= ((pt.y + getHeight()) - desktopSize.height);
367- }
368-
369- setLocation(pt);
370-
371- // 高さはメインフレームと同じにする.
372- Dimension siz = getSize();
373- siz.height = getParent().getHeight() - offset_y;
374- setSize(siz);
375- }
376-
377- /**
378341 * 「選択」ボタンまたはテーブルのダブルクリックのハンドラ.<br>
379342 * 選択されている行のパーツ識別子をもとに、パーツにフォーカスをあてる.<br>
380343 */
--- trunk/src/charactermanaj/ui/MainFrame.java (revision 91)
+++ trunk/src/charactermanaj/ui/MainFrame.java (revision 92)
@@ -18,21 +18,21 @@
1818 import java.awt.dnd.DropTarget;
1919 import java.awt.event.ActionEvent;
2020 import java.awt.event.ActionListener;
21+import java.awt.event.MouseWheelEvent;
22+import java.awt.event.MouseWheelListener;
2123 import java.awt.event.WindowAdapter;
2224 import java.awt.event.WindowEvent;
2325 import java.awt.image.BufferedImage;
2426 import java.io.File;
2527 import java.io.IOException;
26-import java.io.InputStream;
27-import java.io.OutputStream;
2828 import java.lang.reflect.InvocationTargetException;
2929 import java.net.URI;
3030 import java.util.ArrayList;
3131 import java.util.Collections;
32-import java.util.Comparator;
3332 import java.util.List;
3433 import java.util.Map;
3534 import java.util.Properties;
35+import java.util.TreeMap;
3636 import java.util.UUID;
3737 import java.util.logging.Level;
3838 import java.util.logging.Logger;
@@ -47,6 +47,7 @@
4747 import javax.swing.JMenuItem;
4848 import javax.swing.JOptionPane;
4949 import javax.swing.JPanel;
50+import javax.swing.JPopupMenu;
5051 import javax.swing.JScrollBar;
5152 import javax.swing.JScrollPane;
5253 import javax.swing.JSeparator;
@@ -85,8 +86,7 @@
8586 import charactermanaj.model.io.PartsImageDirectoryWatchEvent;
8687 import charactermanaj.model.io.PartsImageDirectoryWatchListener;
8788 import charactermanaj.model.io.RecentDataPersistent;
88-import charactermanaj.model.io.WorkingSetXMLReader;
89-import charactermanaj.model.io.WorkingSetXMLWriter;
89+import charactermanaj.model.io.WorkingSetPersist;
9090 import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelEvent;
9191 import charactermanaj.ui.ImageSelectPanel.ImageSelectPanelListener;
9292 import charactermanaj.ui.ManageFavoriteDialog.FavoriteManageCallback;
@@ -95,6 +95,9 @@
9595 import charactermanaj.ui.model.ColorChangeEvent;
9696 import charactermanaj.ui.model.ColorChangeListener;
9797 import charactermanaj.ui.model.ColorGroupCoordinator;
98+import charactermanaj.ui.model.FavoritesChangeEvent;
99+import charactermanaj.ui.model.FavoritesChangeListener;
100+import charactermanaj.ui.model.FavoritesChangeObserver;
98101 import charactermanaj.ui.model.PartsColorCoordinator;
99102 import charactermanaj.ui.model.PartsSelectionManager;
100103 import charactermanaj.ui.model.WallpaperFactory;
@@ -101,14 +104,14 @@
101104 import charactermanaj.ui.model.WallpaperFactoryErrorRecoverHandler;
102105 import charactermanaj.ui.model.WallpaperFactoryException;
103106 import charactermanaj.ui.model.WallpaperInfo;
107+import charactermanaj.ui.scrollablemenu.JScrollableMenu;
104108 import charactermanaj.ui.util.FileDropTarget;
109+import charactermanaj.ui.util.WindowAdjustLocationSupport;
105110 import charactermanaj.util.DesktopUtilities;
106111 import charactermanaj.util.ErrorMessageHelper;
107112 import charactermanaj.util.LocalizedResourcePropertyLoader;
108113 import charactermanaj.util.SystemUtil;
109114 import charactermanaj.util.UIHelper;
110-import charactermanaj.util.UserData;
111-import charactermanaj.util.UserDataFactory;
112115
113116
114117 /**
@@ -117,7 +120,7 @@
117120 *
118121 * @author seraphy
119122 */
120-public class MainFrame extends JFrame {
123+public class MainFrame extends JFrame implements FavoritesChangeListener {
121124
122125 private static final long serialVersionUID = 1L;
123126
@@ -128,8 +131,6 @@
128131
129132 protected static final String MENU_STRINGS_RESOURCE = "menu/menu";
130133
131- public static final String WORKINGSET_FILE_SUFFIX = "workingset.xml";
132-
133134 /**
134135 * メインフレームのアイコン.<br>
135136 */
@@ -235,6 +236,13 @@
235236 private SearchPartsDialog lastUseSearchPartsDialog;
236237
237238 /**
239+ * 最後に使用したお気に入りダイアログ.<br>
240+ * nullであれば一度も使用していない.<br>
241+ * (nullでなくとも閉じられている可能性がある.)
242+ */
243+ private ManageFavoriteDialog lastUseManageFavoritesDialog;
244+
245+ /**
238246 * 最後に使用した壁紙情報
239247 */
240248 private WallpaperInfo wallpaperInfo;
@@ -312,7 +320,8 @@
312320 // パーツ及びお気に入りを再取得する場合.
313321 try {
314322 Cursor oldCur = mainFrame.getCursor();
315- caller.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
323+ mainFrame.setCursor(Cursor
324+ .getPredefinedCursor(Cursor.WAIT_CURSOR));
316325 try {
317326 mainFrame.reloadPartsAndFavorites(newCd, true);
318327
@@ -418,30 +427,23 @@
418427 }
419428
420429 /**
421- * お気に入りデータが変更されたことを通知される.
430+ * お気に入りデータが変更された場合に通知される.
422431 *
423- * @param cd
424- * キャラクターデータ
432+ * @param e
425433 */
426- public static void notifyChangeFavorites(CharacterData cd) {
427- if (cd == null) {
428- throw new IllegalArgumentException();
429- }
434+ public void notifyChangeFavorites(FavoritesChangeEvent e) {
435+ CharacterData cd = e.getCharacterData();
436+ if (cd.getDocBase().equals(MainFrame.this.characterData.getDocBase())) {
437+ if (!MainFrame.this.equals(e.getSource())) {
438+ // お気に入りを最新化する.
439+ // (ただし、自分自身から送信したイベントの場合はリロードの必要はない)
440+ refreshFavorites();
441+ }
430442
431- // Frameのうち、ネイティブリソースと関連づけられているアクティブなフレームを調査
432- for (Frame frame : JFrame.getFrames()) {
433- if (frame.isDisplayable() && frame instanceof MainFrame) {
434- MainFrame mainFrame = (MainFrame) frame;
435- if (cd.getDocBase().equals(mainFrame.characterData.getDocBase())) {
436- mainFrame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
437- try {
438- // お気に入りを最新の状態に読み直し
439- mainFrame.refreshFavorites();
440-
441- } finally {
442- mainFrame.setCursor(Cursor.getDefaultCursor());
443- }
444- }
443+ // お気に入り管理ダイアログ上のお気に入り一覧を最新に更新する.
444+ if (lastUseManageFavoritesDialog != null
445+ && lastUseManageFavoritesDialog.isDisplayable()) {
446+ lastUseManageFavoritesDialog.initListModel();
445447 }
446448 }
447449 }
@@ -487,6 +489,10 @@
487489 JMenuBar menuBar = createMenuBar();
488490 setJMenuBar(menuBar);
489491
492+ // お気に入り変更通知を受け取る
493+ FavoritesChangeObserver.getDefault().addFavoritesChangeListener(
494+ this);
495+
490496 // メインスクリーンサイズを取得する.
491497 GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
492498 Rectangle desktopSize = genv.getMaximumWindowBounds(); // メインスクリーンのサイズ(デスクトップ領域のみ)
@@ -592,6 +598,8 @@
592598 */
593599 @Override
594600 public void dispose() {
601+ FavoritesChangeObserver.getDefault()
602+ .removeFavoritesChangeListener(this);
595603 imageLoader.close();
596604 stopAgents();
597605 super.dispose();
@@ -647,6 +655,9 @@
647655 // 開いている検索ダイアログを閉じる
648656 closeSearchDialog();
649657
658+ // 開いているお気に入り管理ダイアログを閉じる
659+ closeManageFavoritesDialog();
660+
650661 PartsColorManager partsColorManager = characterData.getPartsColorManager();
651662
652663 // デフォルトの背景色の設定
@@ -677,7 +688,14 @@
677688 previewPane.setTitle(defaultPartsSetTitle);
678689 previewPane.addPreviewPanelListener(new PreviewPanelListener() {
679690 public void addFavorite(PreviewPanelEvent e) {
680- onRegisterFavorite();
691+ if (!e.isShiftKeyPressed()) {
692+ // お気に入り登録
693+ onRegisterFavorite();
694+
695+ } else {
696+ // シフトキーにて、お気に入りの管理を開く
697+ onManageFavorites();
698+ }
681699 }
682700 public void changeBackgroundColor(PreviewPanelEvent e) {
683701 if ( !e.isShiftKeyPressed()) {
@@ -761,7 +779,8 @@
761779 final int curidx = idx;
762780 imageSelectPanel.addImageSelectListener(new ImageSelectPanelListener() {
763781 public void onChangeColor(ImageSelectPanelEvent event) {
764- colorDialog.adjustLocation(curidx);
782+ WindowAdjustLocationSupport.alignRight(
783+ MainFrame.this, colorDialog, curidx, false);
765784 colorDialog.setVisible(!colorDialog.isVisible());
766785 }
767786 public void onPreferences(ImageSelectPanelEvent event) {
@@ -1020,64 +1039,276 @@
10201039 protected List<PartsSet> getPartsSetList() {
10211040 ArrayList<PartsSet> partssets = new ArrayList<PartsSet>();
10221041 partssets.addAll(characterData.getPartsSets().values());
1023- Collections.sort(partssets, new Comparator<PartsSet>() {
1024- public int compare(PartsSet o1, PartsSet o2) {
1025- int ret = o1.getLocalizedName().compareTo(o2.getLocalizedName());
1026- if (ret == 0) {
1027- ret = o1.getPartsSetId().compareTo(o2.getPartsSetId());
1042+ Collections.sort(partssets, PartsSet.DEFAULT_COMPARATOR);
1043+ return partssets;
1044+ }
1045+
1046+ protected static final class TreeLeaf implements Comparable<TreeLeaf> {
1047+
1048+ public enum TreeLeafType {
1049+ NODE, LEAF
1050+ }
1051+
1052+ private String name;
1053+
1054+ private TreeLeafType typ;
1055+
1056+ public TreeLeaf(TreeLeafType typ, String name) {
1057+ if (name == null) {
1058+ name = "";
1059+ }
1060+ this.typ = typ;
1061+ this.name = name;
1062+ }
1063+
1064+ public String getName() {
1065+ return name;
1066+ }
1067+
1068+ public TreeLeafType getTyp() {
1069+ return typ;
1070+ }
1071+
1072+ @Override
1073+ public boolean equals(Object obj) {
1074+ if (obj != null && obj instanceof TreeLeaf) {
1075+ TreeLeaf o = (TreeLeaf) obj;
1076+ return typ == o.typ && name.equals(o.name);
1077+ }
1078+ return false;
1079+ }
1080+
1081+ @Override
1082+ public int hashCode() {
1083+ return typ.hashCode() ^ name.hashCode();
1084+ }
1085+
1086+ public int compareTo(TreeLeaf o) {
1087+ int ret = name.compareTo(o.name);
1088+ if (ret == 0) {
1089+ ret = (typ.ordinal() - o.typ.ordinal());
1090+ }
1091+ return ret;
1092+ }
1093+
1094+ @Override
1095+ public String toString() {
1096+ return name;
1097+ }
1098+ }
1099+
1100+ protected TreeMap<TreeLeaf, Object> buildFavoritesItemTree(
1101+ List<PartsSet> partssets) {
1102+ if (partssets == null) {
1103+ partssets = Collections.emptyList();
1104+ }
1105+ TreeMap<TreeLeaf, Object> favTree = new TreeMap<TreeLeaf, Object>();
1106+ for (PartsSet partsSet : partssets) {
1107+ String flatname = partsSet.getLocalizedName();
1108+ String[] tokens = flatname.split("\\|");
1109+ if (tokens.length == 0) {
1110+ continue;
1111+ }
1112+
1113+ TreeMap<TreeLeaf, Object> r = favTree;
1114+ for (int idx = 0; idx < tokens.length - 1; idx++) {
1115+ String name = tokens[idx];
1116+ TreeLeaf leafName = new TreeLeaf(TreeLeaf.TreeLeafType.NODE,
1117+ name);
1118+ @SuppressWarnings("unchecked")
1119+ TreeMap<TreeLeaf, Object> n = (TreeMap<TreeLeaf, Object>) r
1120+ .get(leafName);
1121+ if (n == null) {
1122+ n = new TreeMap<TreeLeaf, Object>();
1123+ r.put(leafName, n);
10281124 }
1029- if (ret == 0) {
1030- ret = o1.hashCode() - o2.hashCode();
1125+ r = n;
1126+ }
1127+ String lastName = tokens[tokens.length - 1];
1128+ TreeLeaf lastLeafName = new TreeLeaf(TreeLeaf.TreeLeafType.LEAF,
1129+ lastName);
1130+ @SuppressWarnings("unchecked")
1131+ List<PartsSet> leafValue = (List<PartsSet>) r.get(lastLeafName);
1132+ if (leafValue == null) {
1133+ leafValue = new ArrayList<PartsSet>();
1134+ r.put(lastLeafName, leafValue);
1135+ }
1136+ leafValue.add(partsSet);
1137+ }
1138+ return favTree;
1139+ }
1140+
1141+ protected interface FavoriteMenuItemBuilder {
1142+ JMenuItem createFavoriteMenuItem(String name, PartsSet partsSet);
1143+ JMenu createSubMenu(String name);
1144+ }
1145+
1146+ private void buildFavoritesMenuItems(List<JMenuItem> menuItems,
1147+ FavoriteMenuItemBuilder favMenuItemBuilder,
1148+ TreeMap<TreeLeaf, Object> favTree) {
1149+ for (Map.Entry<TreeLeaf, Object> entry : favTree.entrySet()) {
1150+ TreeLeaf treeLeaf = entry.getKey();
1151+ String name = treeLeaf.getName();
1152+ if (treeLeaf.getTyp() == TreeLeaf.TreeLeafType.LEAF) {
1153+ // 葉ノードには、JMenuItemを設定する.
1154+ @SuppressWarnings("unchecked")
1155+ List<PartsSet> leafValue = (List<PartsSet>) entry.getValue();
1156+ for (final PartsSet partsSet : leafValue) {
1157+ JMenuItem favoriteMenu = favMenuItemBuilder
1158+ .createFavoriteMenuItem(name, partsSet);
1159+ menuItems.add(favoriteMenu);
10311160 }
1032- return ret;
1161+
1162+ } else if (treeLeaf.getTyp() == TreeLeaf.TreeLeafType.NODE) {
1163+ // 枝ノードは、サブメニューを作成し、子ノードを設定する
1164+ @SuppressWarnings("unchecked")
1165+ TreeMap<TreeLeaf, Object> childNode = (TreeMap<TreeLeaf, Object>) entry
1166+ .getValue();
1167+ JMenu subMenu = favMenuItemBuilder.createSubMenu(name);
1168+ menuItems.add(subMenu);
1169+ ArrayList<JMenuItem> subMenuItems = new ArrayList<JMenuItem>();
1170+ buildFavoritesMenuItems(subMenuItems, favMenuItemBuilder, childNode);
1171+ for (JMenuItem subMenuItem : subMenuItems) {
1172+ subMenu.add(subMenuItem);
1173+ }
1174+
1175+ } else {
1176+ throw new RuntimeException("unknown type: " + treeLeaf);
10331177 }
1034- });
1035- return partssets;
1178+ }
10361179 }
10371180
10381181 /**
1039- * お気に入りメニューが開いたとき
1040- *
1041- * @param menu
1182+ * お気に入りのJMenuItemを作成するファンクションオブジェクト
10421183 */
1043- protected void onSelectedFavoriteMenu(JMenu menu) {
1044- int mx = menu.getMenuComponentCount();
1045- int separatorIdx = -1;
1046- for (int idx = 0; idx < mx; idx++) {
1047- Component item = menu.getMenuComponent(idx);
1048- if (item instanceof JSeparator) {
1049- separatorIdx = idx;
1050- break;
1184+ private FavoriteMenuItemBuilder favMenuItemBuilder = new FavoriteMenuItemBuilder() {
1185+ private MenuBuilder menuBuilder = new MenuBuilder();
1186+
1187+ /**
1188+ * お気に入りメニューの作成
1189+ */
1190+ public JMenuItem createFavoriteMenuItem(final String name,
1191+ final PartsSet partsSet) {
1192+ JMenuItem favoriteMenu = menuBuilder.createJMenuItem();
1193+ favoriteMenu.setName(partsSet.getPartsSetId());
1194+ favoriteMenu.setText(name);
1195+ if (partsSet.isPresetParts()) {
1196+ Font font = favoriteMenu.getFont();
1197+ favoriteMenu.setFont(font.deriveFont(Font.BOLD));
10511198 }
1199+ favoriteMenu.addActionListener(new ActionListener() {
1200+ public void actionPerformed(ActionEvent e) {
1201+ selectPresetParts(partsSet);
1202+ }
1203+ });
1204+
1205+ // メニューアイテム上でマウスホイールを動かした場合は上下にスクロールさせる.
1206+ // (ただし、OSXのスクリーンメニュー使用時は無視する.)
1207+ addMouseWheelListener(favoriteMenu);
1208+
1209+ return favoriteMenu;
10521210 }
1053- // 既存メニューの削除
1054- if (separatorIdx > 0) {
1055- while (menu.getMenuComponentCount() > separatorIdx + 1) {
1056- menu.remove(separatorIdx + 1);
1211+
1212+ /**
1213+ * サブメニューの作成
1214+ */
1215+ public JMenu createSubMenu(String name) {
1216+ JMenu menu = menuBuilder.createJMenu();
1217+ menu.setText(name);
1218+
1219+ // メニューアイテム上でマウスホイールを動かした場合は上下にスクロールさせる.
1220+ // (ただし、OSXのスクリーンメニュー使用時は無視する.)
1221+ addMouseWheelListener(menu);
1222+
1223+ return menu;
1224+ }
1225+
1226+ /**
1227+ * メニューアイテム上でホイールを上下させたときにメニューをスクロールさせるためのホイールハンドラを設定する.
1228+ *
1229+ * @param favoriteMenu
1230+ */
1231+ protected void addMouseWheelListener(final JMenuItem favoriteMenu) {
1232+ if (JScrollableMenu.isScreenMenu()) {
1233+ return;
10571234 }
1235+ favoriteMenu.addMouseWheelListener(new MouseWheelListener() {
1236+ public void mouseWheelMoved(MouseWheelEvent e) {
1237+ int rotation = e.getWheelRotation();
1238+ JPopupMenu popupMenu = (JPopupMenu) favoriteMenu
1239+ .getParent();
1240+ JMenu parentMenu = (JMenu) popupMenu.getInvoker();
1241+ if (parentMenu != null
1242+ && parentMenu instanceof JScrollableMenu) {
1243+ final JScrollableMenu favMenu = (JScrollableMenu) parentMenu;
1244+ favMenu.doScroll(rotation < 0);
1245+ }
1246+ e.consume();
1247+ }
1248+ });
10581249 }
1250+ };
10591251
1252+ /**
1253+ * お気に入りメニューが開いたとき
1254+ *
1255+ * @param menu
1256+ */
1257+ protected void onSelectedFavoriteMenu(JMenu menu) {
10601258 // 表示順にソート
10611259 List<PartsSet> partssets = getPartsSetList();
1260+ TreeMap<TreeLeaf, Object> favTree = buildFavoritesItemTree(partssets);
10621261
10631262 // メニューの再構築
1263+ ArrayList<JMenuItem> favoritesMenuItems = new ArrayList<JMenuItem>();
1264+ buildFavoritesMenuItems(favoritesMenuItems, favMenuItemBuilder, favTree);
10641265
1065- MenuBuilder menuBuilder = new MenuBuilder();
1066- for (final PartsSet presetParts : partssets) {
1067- JMenuItem favoriteMenu = menuBuilder.createJMenuItem();
1068- favoriteMenu.setName(presetParts.getPartsSetId());
1069- favoriteMenu.setText(presetParts.getLocalizedName());
1070- if (presetParts.isPresetParts()) {
1071- Font font = favoriteMenu.getFont();
1072- favoriteMenu.setFont(font.deriveFont(Font.BOLD));
1266+ if (menu instanceof JScrollableMenu) {
1267+ // スクロールメニューの場合
1268+ JScrollableMenu favMenu = (JScrollableMenu) menu;
1269+
1270+ // スクロールメニューの初期化
1271+ favMenu.initScroller();
1272+
1273+ // スクロールメニューアイテムの設定
1274+ favMenu.setScrollableItems(favoritesMenuItems);
1275+
1276+ // 高さを補正する
1277+ // お気に入りメニューが選択された場合、
1278+ // お気に入りアイテム一覧を表示するよりも前に
1279+ // 表示可能なアイテム数を現在のウィンドウの高さから算定する.
1280+ Toolkit tk = Toolkit.getDefaultToolkit();
1281+ Dimension scrsiz = tk.getScreenSize();
1282+ int height = scrsiz.height; // MainFrame.this.getHeight();
1283+ favMenu.adjustMaxVisible(height);
1284+ logger.log(Level.FINE,
1285+ "scrollableMenu maxVisible=" + favMenu.getMaxVisible());
1286+
1287+ } else {
1288+ // 通常メニューの場合
1289+ // 既存メニューの位置をセパレータより判断する.
1290+ int mx = menu.getMenuComponentCount();
1291+ int separatorIdx = -1;
1292+ for (int idx = 0; idx < mx; idx++) {
1293+ Component item = menu.getMenuComponent(idx);
1294+ if (item instanceof JSeparator) {
1295+ separatorIdx = idx;
1296+ break;
1297+ }
10731298 }
1074- favoriteMenu.addActionListener(new ActionListener() {
1075- public void actionPerformed(ActionEvent e) {
1076- selectPresetParts(presetParts);
1299+ // 既存メニューの削除
1300+ if (separatorIdx > 0) {
1301+ while (menu.getMenuComponentCount() > separatorIdx + 1) {
1302+ menu.remove(separatorIdx + 1);
10771303 }
1078- });
1079- menu.add(favoriteMenu);
1304+ }
1305+
1306+ // お気に入りアイテムのメニューを登録する.
1307+ for (JMenuItem menuItem : favoritesMenuItems) {
1308+ menu.add(menuItem);
1309+ }
10801310 }
1311+
10811312 }
10821313
10831314 /**
@@ -1545,7 +1776,7 @@
15451776 }
15461777
15471778 SearchPartsDialog searchPartsDlg = new SearchPartsDialog(this, characterData, partsSelectionManager);
1548- searchPartsDlg.adjustLocation(0);
1779+ WindowAdjustLocationSupport.alignRight(this, searchPartsDlg, 0, true);
15491780 searchPartsDlg.setVisible(true);
15501781 lastUseSearchPartsDialog = searchPartsDlg;
15511782 }
@@ -1554,7 +1785,7 @@
15541785 * 「パーツ検索」ダイアログを閉じる.<br>
15551786 */
15561787 protected void closeSearchDialog() {
1557- lastUsePresetParts = null;
1788+ lastUseSearchPartsDialog = null;
15581789 for (SearchPartsDialog dlg : SearchPartsDialog.getDialogs()) {
15591790 if (dlg != null && dlg.isDisplayable() && dlg.getParent() == this) {
15601791 dlg.dispose();
@@ -1563,6 +1794,18 @@
15631794 }
15641795
15651796 /**
1797+ * 「お気に入りの管理」ダイアログを閉じる
1798+ */
1799+ protected void closeManageFavoritesDialog() {
1800+ if (lastUseManageFavoritesDialog != null) {
1801+ if (lastUseManageFavoritesDialog.isDisplayable()) {
1802+ lastUseManageFavoritesDialog.dispose();
1803+ }
1804+ lastUseManageFavoritesDialog = null;
1805+ }
1806+ }
1807+
1808+ /**
15661809 * クリップボードにコピー
15671810 *
15681811 * @param screenImage
@@ -1704,7 +1947,10 @@
17041947 // お気に入りをリロードする.
17051948 CharacterDataPersistent persiste = CharacterDataPersistent.getInstance();
17061949 persiste.loadFavorites(characterData);
1707- notifyChangeFavorites(characterData);
1950+
1951+ // お気に入りが更新されたことを通知する.
1952+ FavoritesChangeObserver.getDefault().notifyFavoritesChange(
1953+ MainFrame.this, characterData);
17081954 }
17091955
17101956 // 現在選択されているパーツセットがない場合はデフォルトのパーツセットを選択する.
@@ -1843,17 +2089,10 @@
18432089 workingSet.setWallpaperInfo(wallpaperInfo);
18442090
18452091 // XML形式でのワーキングセットの保存
1846- UserDataFactory userDataFactory = UserDataFactory.getInstance();
1847- UserData workingSetXmlData = userDataFactory.getMangledNamedUserData(
1848- characterData.getDocBase(), WORKINGSET_FILE_SUFFIX);
1849- OutputStream outstm = workingSetXmlData.getOutputStream();
1850- try {
1851- WorkingSetXMLWriter workingSetXmlWriter = new WorkingSetXMLWriter();
1852- workingSetXmlWriter.writeWorkingSet(workingSet, outstm);
1853- } finally {
1854- outstm.close();
1855- }
1856-
2092+ WorkingSetPersist workingSetPersist = WorkingSetPersist
2093+ .getInstance();
2094+ workingSetPersist.saveWorkingSet(workingSet);
2095+
18572096 } catch (Exception ex) {
18582097 ErrorMessageHelper.showErrorDialog(this, ex);
18592098 }
@@ -1862,7 +2101,7 @@
18622101 /**
18632102 * 画面の作業状態を復元する.
18642103 *
1865- * @return
2104+ * @return ワーキングセットを読み込んだ場合はtrue、そうでなければfalse
18662105 */
18672106 protected boolean loadWorkingSet() {
18682107 if (!characterData.isValid()) {
@@ -1869,25 +2108,15 @@
18692108 return false;
18702109 }
18712110 try {
1872- // XML形式でのワーキングセットの復元
1873- UserDataFactory userDataFactory = UserDataFactory.getInstance();
1874- UserData workingSetXmlData = userDataFactory.getMangledNamedUserData(
1875- characterData.getDocBase(), WORKINGSET_FILE_SUFFIX);
1876- if (workingSetXmlData == null || !workingSetXmlData.exists()) {
1877- // 保存されていない場合
2111+ WorkingSetPersist workingSetPersist = WorkingSetPersist
2112+ .getInstance();
2113+ WorkingSet2 workingSet2 = workingSetPersist
2114+ .loadWorkingSet(characterData);
2115+ if (workingSet2 == null) {
2116+ // ワーキングセットがない場合.
18782117 return false;
18792118 }
1880- WorkingSet2 workingSet2;
18812119
1882- InputStream is = workingSetXmlData.openStream();
1883- try {
1884- WorkingSetXMLReader WorkingSetXMLReader = new WorkingSetXMLReader();
1885- workingSet2 = WorkingSetXMLReader.loadWorkingSet(is);
1886-
1887- } finally {
1888- is.close();
1889- }
1890-
18912120 URI docBase = characterData.getDocBase();
18922121 if (docBase != null
18932122 && !docBase.equals(workingSet2.getCharacterDocBase())) {
@@ -2027,38 +2256,60 @@
20272256 return;
20282257 }
20292258
2030- // お気に入りの状態を最新にリフレッシュする.
2031- refreshFavorites();
2259+ if (lastUseManageFavoritesDialog != null) {
2260+ // 開いているダイアログがあれば、それにフォーカスを当てる.
2261+ if (lastUseManageFavoritesDialog.isDisplayable()
2262+ && lastUseManageFavoritesDialog.isVisible()) {
2263+ lastUseManageFavoritesDialog.requestFocus();
2264+ return;
2265+ }
2266+ }
20322267
20332268 // お気に入り編集ダイアログを開く
20342269 ManageFavoriteDialog dlg = new ManageFavoriteDialog(this, characterData);
20352270 dlg.setFavoriteManageCallback(new FavoriteManageCallback() {
2271+
2272+ public void refreshFavorites(CharacterData cd) {
2273+ // お気に入りの状態を最新にリフレッシュする.
2274+ MainFrame.this.refreshFavorites();
2275+ }
2276+
20362277 public void selectFavorites(PartsSet partsSet) {
20372278 // お気に入り編集ダイアログで選択されたパーツを選択表示する.
20382279 selectPresetParts(partsSet);
20392280 }
2040- });
2041- dlg.setVisible(true);
2042- if (!dlg.isModified()) {
2043- return;
2044- }
20452281
2046- // お気に入りを登録する.
2047- try {
2048- setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
2049- try {
2050- CharacterDataPersistent persiste = CharacterDataPersistent.getInstance();
2051- persiste.saveFavorites(characterData);
2282+ public void updateFavorites(CharacterData characterData,
2283+ boolean savePreset) {
2284+ // お気に入りを登録する.
2285+ try {
2286+ setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
2287+ try {
2288+ CharacterDataPersistent persiste = CharacterDataPersistent
2289+ .getInstance();
2290+ if (savePreset) {
2291+ persiste.updateProfile(characterData);
2292+ }
20522293
2053- notifyChangeFavorites(characterData);
2294+ persiste.saveFavorites(characterData);
20542295
2055- } finally {
2056- setCursor(Cursor.getDefaultCursor());
2296+ // お気に入りが更新されたことを通知する.
2297+ FavoritesChangeObserver.getDefault()
2298+ .notifyFavoritesChange(MainFrame.this,
2299+ characterData);
2300+
2301+ } finally {
2302+ setCursor(Cursor.getDefaultCursor());
2303+ }
2304+
2305+ } catch (Exception ex) {
2306+ ErrorMessageHelper.showErrorDialog(MainFrame.this, ex);
2307+ }
20572308 }
2058-
2059- } catch (Exception ex) {
2060- ErrorMessageHelper.showErrorDialog(this, ex);
2061- }
2309+ });
2310+ WindowAdjustLocationSupport.alignRight(this, dlg, 0, true);
2311+ dlg.setVisible(true);
2312+ lastUseManageFavoritesDialog = dlg;
20622313 }
20632314
20642315 /**
@@ -2160,7 +2411,9 @@
21602411
21612412 persiste.saveFavorites(characterData);
21622413
2163- notifyChangeFavorites(characterData);
2414+ // お気に入りが更新されたことを通知する.
2415+ FavoritesChangeObserver.getDefault().notifyFavoritesChange(
2416+ MainFrame.this, characterData);
21642417
21652418 } finally {
21662419 setCursor(Cursor.getDefaultCursor());
--- trunk/src/charactermanaj/ui/ManageFavoriteDialog.java (revision 91)
+++ trunk/src/charactermanaj/ui/ManageFavoriteDialog.java (revision 92)
@@ -1,11 +1,11 @@
11 package charactermanaj.ui;
22
33 import java.awt.BorderLayout;
4-import java.awt.Component;
54 import java.awt.Container;
65 import java.awt.Dimension;
76 import java.awt.GridBagConstraints;
87 import java.awt.GridBagLayout;
8+import java.awt.Point;
99 import java.awt.Toolkit;
1010 import java.awt.event.ActionEvent;
1111 import java.awt.event.KeyEvent;
@@ -14,9 +14,10 @@
1414 import java.awt.event.WindowAdapter;
1515 import java.awt.event.WindowEvent;
1616 import java.util.ArrayList;
17+import java.util.Arrays;
1718 import java.util.Collections;
18-import java.util.Comparator;
1919 import java.util.Iterator;
20+import java.util.List;
2021 import java.util.Map;
2122 import java.util.Properties;
2223
@@ -25,21 +26,25 @@
2526 import javax.swing.ActionMap;
2627 import javax.swing.BorderFactory;
2728 import javax.swing.Box;
28-import javax.swing.DefaultListCellRenderer;
29-import javax.swing.DefaultListModel;
3029 import javax.swing.InputMap;
3130 import javax.swing.JButton;
3231 import javax.swing.JComponent;
3332 import javax.swing.JDialog;
3433 import javax.swing.JFrame;
35-import javax.swing.JList;
34+import javax.swing.JMenuItem;
3635 import javax.swing.JOptionPane;
3736 import javax.swing.JPanel;
37+import javax.swing.JPopupMenu;
3838 import javax.swing.JRootPane;
3939 import javax.swing.JScrollPane;
40+import javax.swing.JTable;
4041 import javax.swing.KeyStroke;
41-import javax.swing.ListCellRenderer;
42+import javax.swing.ListSelectionModel;
43+import javax.swing.SwingUtilities;
4244 import javax.swing.UIManager;
45+import javax.swing.event.ListSelectionEvent;
46+import javax.swing.event.ListSelectionListener;
47+import javax.swing.table.AbstractTableModel;
4348
4449 import charactermanaj.model.CharacterData;
4550 import charactermanaj.model.PartsSet;
@@ -59,20 +64,149 @@
5964
6065 private CharacterData characterData;
6166
62- private DefaultListModel listModel;
67+ private PartsSetListTableModel partsSetListModel;
6368
64- private JList list;
65-
66- private boolean dirty;
67-
69+ private JTable partsSetList;
70+
6871 private FavoriteManageCallback callback;
6972
73+ private Action actSelect;
74+
75+ private Action actDelete;
76+
77+ private Action actRename;
78+
79+ public static class PartsSetListTableModel extends AbstractTableModel {
80+
81+ /**
82+ * シリアライズバージョンID
83+ */
84+ private static final long serialVersionUID = 3012538368342673506L;
85+
86+ /**
87+ * パーツセットのリスト
88+ */
89+ private List<PartsSet> partsSetList = Collections.emptyList();
90+
91+ private enum Columns {
92+ DISPLAY_NAME("Name") {
93+ @Override
94+ public Object getValue(PartsSet partsSet) {
95+ if (partsSet != null) {
96+ return partsSet.getLocalizedName();
97+ }
98+ return null;
99+ }
100+ },
101+ IS_PRESET("Type") {
102+ @Override
103+ public Object getValue(PartsSet partsSet) {
104+ if (partsSet != null) {
105+ return partsSet.isPresetParts()
106+ ? "Preset"
107+ : "Favorites";
108+ }
109+ return null;
110+ }
111+ };
112+
113+ private String columnName;
114+
115+ private Columns(String columnName) {
116+ this.columnName = columnName;
117+ }
118+
119+ public Class<?> getColumnClass() {
120+ return String.class;
121+ }
122+
123+ public String getColumnName() {
124+ return columnName;
125+ }
126+
127+ public abstract Object getValue(PartsSet partsSet);
128+ }
129+
130+ private static Columns[] columns = Columns.values();
131+
132+ public int getColumnCount() {
133+ return columns.length;
134+ }
135+
136+ public int getRowCount() {
137+ return partsSetList.size();
138+ }
139+
140+ public Object getValueAt(int rowIndex, int columnIndex) {
141+ PartsSet partsSet = getRow(rowIndex);
142+ return columns[columnIndex].getValue(partsSet);
143+ }
144+
145+ @Override
146+ public Class<?> getColumnClass(int columnIndex) {
147+ return columns[columnIndex].getColumnClass();
148+ }
149+
150+ @Override
151+ public String getColumnName(int column) {
152+ return columns[column].getColumnName();
153+ }
154+
155+ public PartsSet getRow(int rowIndex) {
156+ return partsSetList.get(rowIndex);
157+ }
158+
159+ public void updateRow(int rowIndex, PartsSet partsSet) {
160+ partsSetList.set(rowIndex, partsSet);
161+ fireTableRowsUpdated(rowIndex, rowIndex);
162+ }
163+
164+ public List<PartsSet> getPartsSetList() {
165+ return new ArrayList<PartsSet>(partsSetList);
166+ }
167+
168+ public void setPartsSetList(List<PartsSet> partsSetList) {
169+ if (partsSetList == null) {
170+ partsSetList = Collections.emptyList();
171+ }
172+ this.partsSetList = new ArrayList<PartsSet>(partsSetList);
173+ fireTableDataChanged();
174+ }
175+ }
176+
177+ /**
178+ * パーツセットの選択および保存を行うためのコールバック.
179+ */
70180 public interface FavoriteManageCallback {
181+
182+ /**
183+ * キャラクターデータのお気に入り状態を最新にする.
184+ *
185+ * @param cd
186+ */
187+ void refreshFavorites(CharacterData cd);
188+
189+ /**
190+ * 引数で指定されたパーツセットを表示する.
191+ *
192+ * @param partsSet
193+ */
71194 void selectFavorites(PartsSet partsSet);
195+
196+ /**
197+ * 指定したキャラクターデータのお気に入りを保存する.<br>
198+ * presetを変更した場合はcharacter.xmlを更新するためにsavePreset引数をtrueとする.<br>
199+ *
200+ * @param characterData
201+ * お気に入りを保存するキャラクターデータ
202+ * @param savePreset
203+ * character.xmlを更新する場合(presetの更新)
204+ */
205+ void updateFavorites(CharacterData characterData, boolean savePreset);
72206 }
73207
74208 public ManageFavoriteDialog(JFrame parent, CharacterData characterData) {
75- super(parent, true);
209+ super(parent, false);
76210 if (characterData == null) {
77211 throw new IllegalArgumentException();
78212 }
@@ -98,34 +232,35 @@
98232 Container contentPane = getContentPane();
99233 contentPane.setLayout(new BorderLayout());
100234
101- listModel = new DefaultListModel();
102- list = new JList(listModel);
235+ partsSetListModel = new PartsSetListTableModel();
236+ partsSetList = new JTable(partsSetListModel);
237+ partsSetList.setRowSelectionAllowed(true);
238+ partsSetList
239+ .setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
240+
241+ partsSetList.setTableHeader(null);
242+ partsSetList.getColumnModel().getColumn(1).setMaxWidth(150);
103243
104- ListCellRenderer listCellRenderer = new DefaultListCellRenderer() {
244+ partsSetList.getSelectionModel().addListSelectionListener(
245+ new ListSelectionListener() {
246+ public void valueChanged(ListSelectionEvent e) {
247+ updateButtonUI();
248+ }
249+ });
250+
251+ actSelect = new AbstractAction(strings.getProperty("select")) {
105252 private static final long serialVersionUID = 1L;
106- @Override
107- public Component getListCellRendererComponent(JList list,
108- Object value, int index, boolean isSelected,
109- boolean cellHasFocus) {
110- Object dispayValue = ((PartsSet) value).getLocalizedName();
111- return super.getListCellRendererComponent(list, dispayValue, index, isSelected,
112- cellHasFocus);
113- }
114- };
115- list.setCellRenderer(listCellRenderer);
116- AbstractAction actSelect = new AbstractAction(strings.getProperty("select")) {
117- private static final long serialVersionUID = 1L;
118253 public void actionPerformed(ActionEvent e) {
119254 onSelect();
120255 }
121256 };
122- AbstractAction actDelete = new AbstractAction(strings.getProperty("remove")) {
257+ actDelete = new AbstractAction(strings.getProperty("remove")) {
123258 private static final long serialVersionUID = 1L;
124259 public void actionPerformed(ActionEvent e) {
125260 onDelete();
126261 }
127262 };
128- AbstractAction actRename = new AbstractAction(strings.getProperty("rename")) {
263+ actRename = new AbstractAction(strings.getProperty("rename")) {
129264 private static final long serialVersionUID = 1L;
130265 public void actionPerformed(ActionEvent e) {
131266 onRename();
@@ -169,7 +304,7 @@
169304 JButton btnClose = new JButton(actCancel);
170305 panel2.add(btnClose, BorderLayout.EAST);
171306
172- JScrollPane scr = new JScrollPane(list);
307+ JScrollPane scr = new JScrollPane(partsSetList);
173308 scr.setBorder(BorderFactory.createEtchedBorder());
174309 scr.setPreferredSize(new Dimension(300, 150));
175310
@@ -188,10 +323,15 @@
188323 am.put("deleteFav", actDelete);
189324 am.put("closeManageFavoriteDialog", actCancel);
190325
191- pack();
326+ setSize(400, 500);
192327 setLocationRelativeTo(parent);
193328
194- list.addMouseListener(new MouseAdapter() {
329+ final JPopupMenu popupMenu = new JPopupMenu();
330+ popupMenu.add(new JMenuItem(actSelect));
331+ popupMenu.add(new JMenuItem(actRename));
332+ popupMenu.add(new JMenuItem(actDelete));
333+
334+ partsSetList.addMouseListener(new MouseAdapter() {
195335 @Override
196336 public void mouseClicked(MouseEvent e) {
197337 if (e.getClickCount() == 2) {
@@ -198,36 +338,79 @@
198338 onSelect();
199339 }
200340 }
341+ @Override
342+ public void mousePressed(MouseEvent e) {
343+ if (SwingUtilities.isRightMouseButton(e)) {
344+ // 右クリックによる選択
345+ Point pt = e.getPoint();
346+ int rowIndex = partsSetList.rowAtPoint(pt);
347+ if (rowIndex >= 0) {
348+ int[] selrows = partsSetList.getSelectedRows();
349+ if (!Arrays.asList(selrows).contains(rowIndex)) {
350+ // 現在の選択行以外を右クリックした場合、その行を選択行とする.
351+ ListSelectionModel selModel = partsSetList
352+ .getSelectionModel();
353+ selModel.setSelectionInterval(rowIndex, rowIndex);
354+ }
355+ }
356+ }
357+ evaluatePopup(e);
358+ }
359+ @Override
360+ public void mouseReleased(MouseEvent e) {
361+ evaluatePopup(e);
362+ }
363+ private void evaluatePopup(MouseEvent e) {
364+ if (e.isPopupTrigger()) {
365+ popupMenu.show(partsSetList, e.getX(), e.getY());
366+ }
367+ }
201368 });
202369
370+ if (callback != null) {
371+ callback.refreshFavorites(this.characterData);
372+ }
373+
203374 initListModel();
204- list.repaint();
375+
376+ updateButtonUI();
205377 }
206378
207- protected void initListModel() {
379+ /**
380+ * 現在のキャラクターデータの最新の状態でお気に入り一覧を更新する.
381+ */
382+ public void initListModel() {
208383 ArrayList<PartsSet> partssets = new ArrayList<PartsSet>();
209384 for (PartsSet partsset : characterData.getPartsSets().values()) {
210- if (!partsset.isPresetParts()) {
211- partssets.add(partsset);
212- }
385+ partssets.add(partsset);
213386 }
214- Collections.sort(partssets, new Comparator<PartsSet>() {
215- public int compare(PartsSet o1, PartsSet o2) {
216- int ret = o1.getLocalizedName().compareTo(o2.getLocalizedName());
217- if (ret == 0) {
218- ret = o1.getPartsSetId().compareTo(o2.getPartsSetId());
219- }
220- if (ret == 0) {
221- ret = o1.hashCode() - o2.hashCode();
222- }
223- return ret;
224- }
225- });
226- list.setSelectedIndices(new int[0]);
227- listModel.removeAllElements();
228- for (PartsSet partsset : partssets) {
229- listModel.addElement(partsset);
387+ Collections.sort(partssets, PartsSet.DEFAULT_COMPARATOR);
388+ partsSetListModel.setPartsSetList(partssets);
389+ }
390+
391+ protected void updateButtonUI() {
392+ int[] rows = partsSetList.getSelectedRows();
393+ actSelect.setEnabled(rows.length == 1);
394+ actRename.setEnabled(rows.length == 1);
395+ actDelete.setEnabled(rows.length >= 1);
396+ }
397+
398+ /**
399+ * 選択されている「お気に入り」のパーツセットの一覧を取得する.<br>
400+ * プリセットが選択されている場合、それは除外される.<br>
401+ *
402+ * @param beep
403+ * プリセットが選択されている場合にビープを鳴らすか?
404+ * @return お気に入りのパーツセットのリスト、選択がなければ空のリスト.
405+ */
406+ protected List<PartsSet> getSelectedPartsSet() {
407+ ArrayList<PartsSet> selectedPartsSet = new ArrayList<PartsSet>();
408+ int[] rows = partsSetList.getSelectedRows();
409+ for (int row : rows) {
410+ PartsSet partsSet = partsSetListModel.getRow(row);
411+ selectedPartsSet.add(partsSet);
230412 }
413+ return selectedPartsSet;
231414 }
232415
233416 /**
@@ -234,6 +417,11 @@
234417 * お気に入りの削除
235418 */
236419 protected void onDelete() {
420+ List<PartsSet> removePartsSet = getSelectedPartsSet();
421+ if (removePartsSet.isEmpty() || callback == null) {
422+ return;
423+ }
424+
237425 // 削除の確認ダイアログ
238426 Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
239427 .getLocalizedProperties(STRINGS_RESOURCE);
@@ -264,10 +452,10 @@
264452 }
265453
266454 // お気に入りリストから削除する.
455+ boolean dirty = false;
456+ boolean deletePreset = false;
267457 Map<String, PartsSet> partsSetMap = characterData.getPartsSets();
268- for (Object value : list.getSelectedValues()) {
269- PartsSet partsSet = (PartsSet) value;
270-
458+ for (PartsSet partsSet : removePartsSet) {
271459 Iterator<Map.Entry<String, PartsSet>> ite = partsSetMap.entrySet().iterator();
272460 while (ite.hasNext()) {
273461 Map.Entry<String, PartsSet> entry = ite.next();
@@ -274,12 +462,18 @@
274462 PartsSet target = entry.getValue();
275463 if (target == partsSet) {
276464 dirty = true;
465+ if (target.isPresetParts()) {
466+ // presetを削除した場合はcharacter.xmlの更新が必要
467+ deletePreset = true;
468+ }
277469 ite.remove();
278470 }
279471 }
280472 }
281- initListModel();
282- list.repaint();
473+ if (dirty) {
474+ callback.updateFavorites(characterData, deletePreset);
475+ initListModel();
476+ }
283477 }
284478
285479 /**
@@ -286,18 +480,21 @@
286480 * お気に入りのリネーム
287481 */
288482 protected void onRename() {
289- PartsSet partsSet = (PartsSet) list.getSelectedValue();
290- if (partsSet != null) {
291- Properties strings = LocalizedResourcePropertyLoader.getCachedInstance()
292- .getLocalizedProperties(STRINGS_RESOURCE);
483+ int row = partsSetList.getSelectedRow();
484+ if (row < 0 || callback == null) {
485+ return;
486+ }
487+ PartsSet partsSet = partsSetListModel.getRow(row);
293488
294- String localizedName = JOptionPane.showInputDialog(this, strings
295- .getProperty("inputName"), partsSet.getLocalizedName());
296- if (localizedName != null) {
297- partsSet.setLocalizedName(localizedName);
298- dirty = true;
299- list.repaint();
300- }
489+ Properties strings = LocalizedResourcePropertyLoader
490+ .getCachedInstance().getLocalizedProperties(STRINGS_RESOURCE);
491+
492+ String localizedName = JOptionPane.showInputDialog(this,
493+ strings.getProperty("inputName"), partsSet.getLocalizedName());
494+ if (localizedName != null) {
495+ partsSet.setLocalizedName(localizedName);
496+ callback.updateFavorites(characterData, partsSet.isPresetParts());
497+ initListModel();
301498 }
302499 }
303500
@@ -305,12 +502,14 @@
305502 * 選択したお気に入りを表示する.
306503 */
307504 protected void onSelect() {
308- PartsSet partsSet = (PartsSet) list.getSelectedValue();
309- if (partsSet != null) {
310- if (callback != null) {
311- callback.selectFavorites(partsSet);
312- }
505+ int row = partsSetList.getSelectedRow();
506+ if (row < 0) {
507+ return;
313508 }
509+ PartsSet partsSet = partsSetListModel.getRow(row);
510+ if (callback != null) {
511+ callback.selectFavorites(partsSet);
512+ }
314513 }
315514
316515 protected void onClose() {
@@ -317,10 +516,6 @@
317516 dispose();
318517 }
319518
320- public boolean isModified() {
321- return dirty;
322- }
323-
324519 public void setFavoriteManageCallback(FavoriteManageCallback callback) {
325520 this.callback = callback;
326521 }
--- trunk/src/charactermanaj/ui/ImportWizardDialog.java (revision 91)
+++ trunk/src/charactermanaj/ui/ImportWizardDialog.java (revision 92)
@@ -1234,6 +1234,8 @@
12341234
12351235 public static final String PANEL_NAME = "importTypeSelectPanel";
12361236
1237+ private ImportWizardDialog parent;
1238+
12371239 private SamplePicturePanel samplePicturePanel;
12381240
12391241 private JTextField txtCharacterId;
@@ -1463,6 +1465,8 @@
14631465
14641466 @Override
14651467 public void onActive(ImportWizardDialog parent, ImportWizardCardPanel previousPanel) {
1468+ this.parent = parent;
1469+
14661470 if (previousPanel == parent.importPartsSelectPanel) {
14671471 return;
14681472 }
@@ -1654,8 +1658,10 @@
16541658 @Override
16551659 public boolean isReadyFinish() {
16561660 if (!isImportPartsImages() && !isImportPreset()) {
1657- if (isImportSampleImage()) {
1658- // 新規プロファイル作成かサンプルイメージの更新のみでイメージもパーツセットもいらなければ、ただちに作成可能.
1661+ if ((parent != null && parent.current == null)
1662+ || isImportSampleImage()) {
1663+ // 新規プロファイル作成か、サンプルイメージの更新のみで
1664+ // イメージもパーツセットもいらなければ、ただちに作成可能.
16591665 return true;
16601666 }
16611667 }
--- trunk/src/charactermanaj/ui/model/FavoritesChangeObserver.java (nonexistent)
+++ trunk/src/charactermanaj/ui/model/FavoritesChangeObserver.java (revision 92)
@@ -0,0 +1,61 @@
1+package charactermanaj.ui.model;
2+
3+import javax.swing.event.EventListenerList;
4+
5+import charactermanaj.model.CharacterData;
6+
7+
8+/**
9+ * お気に入りが変更されたことを通知するためのメカニズム.<br>
10+ *
11+ * @author seraphy
12+ *
13+ */
14+public abstract class FavoritesChangeObserver {
15+
16+ private static FavoritesChangeObserver defobj = new FavoritesChangeObserverImpl();
17+
18+ public static FavoritesChangeObserver getDefault() {
19+ return defobj;
20+ }
21+
22+ public abstract void addFavoritesChangeListener(FavoritesChangeListener l);
23+
24+ public abstract void removeFavoritesChangeListener(FavoritesChangeListener l);
25+
26+ public abstract void notifyFavoritesChange(FavoritesChangeEvent e);
27+
28+ public void notifyFavoritesChange(Object wnd, CharacterData cd) {
29+ if (cd == null) {
30+ throw new IllegalArgumentException();
31+ }
32+ notifyFavoritesChange(new FavoritesChangeEvent(wnd, cd));
33+ }
34+}
35+
36+class FavoritesChangeObserverImpl extends FavoritesChangeObserver {
37+
38+ private EventListenerList listeners = new EventListenerList();
39+
40+ @Override
41+ public void addFavoritesChangeListener(FavoritesChangeListener l) {
42+ listeners.add(FavoritesChangeListener.class, l);
43+ }
44+
45+ @Override
46+ public void removeFavoritesChangeListener(FavoritesChangeListener l) {
47+ listeners.remove(FavoritesChangeListener.class, l);
48+ }
49+
50+ @Override
51+ public void notifyFavoritesChange(FavoritesChangeEvent e) {
52+ if (e == null) {
53+ throw new IllegalArgumentException();
54+ }
55+ FavoritesChangeListener[] lst = listeners
56+ .getListeners(FavoritesChangeListener.class);
57+ for (FavoritesChangeListener l : lst) {
58+ l.notifyChangeFavorites(e);
59+ }
60+ }
61+}
--- trunk/src/charactermanaj/ui/model/FavoritesChangeEvent.java (nonexistent)
+++ trunk/src/charactermanaj/ui/model/FavoritesChangeEvent.java (revision 92)
@@ -0,0 +1,29 @@
1+package charactermanaj.ui.model;
2+
3+import java.util.EventObject;
4+
5+import charactermanaj.model.CharacterData;
6+
7+/**
8+ * お気に入り変更イベント.<br>
9+ *
10+ * @author seraphy
11+ */
12+public class FavoritesChangeEvent extends EventObject {
13+
14+ /**
15+ * シリアライズバージョンID
16+ */
17+ private static final long serialVersionUID = 3206827658882098336L;
18+
19+ private CharacterData characterData;
20+
21+ public FavoritesChangeEvent(Object src, CharacterData characterData) {
22+ super(src);
23+ this.characterData = characterData;
24+ }
25+
26+ public CharacterData getCharacterData() {
27+ return characterData;
28+ }
29+}
--- trunk/src/charactermanaj/ui/model/FavoritesChangeListener.java (nonexistent)
+++ trunk/src/charactermanaj/ui/model/FavoritesChangeListener.java (revision 92)
@@ -0,0 +1,9 @@
1+package charactermanaj.ui.model;
2+
3+import java.util.EventListener;
4+
5+public interface FavoritesChangeListener extends EventListener {
6+
7+ void notifyChangeFavorites(FavoritesChangeEvent e);
8+
9+}
--- trunk/src/charactermanaj/ui/ProfileEditDialog.java (revision 91)
+++ trunk/src/charactermanaj/ui/ProfileEditDialog.java (revision 92)
@@ -1172,7 +1172,10 @@
11721172 chkWatchDir.setSelected(original.isWatchDirectory());
11731173
11741174 // パーツセット
1175- for (PartsSet partsSet : original.getPartsSets().values()) {
1175+ ArrayList<PartsSet> partsSets = new ArrayList<PartsSet>();
1176+ partsSets.addAll(original.getPartsSets().values());
1177+ Collections.sort(partsSets, PartsSet.DEFAULT_COMPARATOR);
1178+ for (PartsSet partsSet : partsSets) {
11761179 partssetsTableModel.addRow(new PresetsTableRow(partsSet));
11771180 }
11781181 partssetsTableModel.setDefaultPartsSetId(original.getDefaultPartsSetId());
--- trunk/src/charactermanaj/ui/MenuBuilder.java (revision 91)
+++ trunk/src/charactermanaj/ui/MenuBuilder.java (revision 92)
@@ -12,10 +12,12 @@
1212 import javax.swing.JMenuItem;
1313 import javax.swing.JSeparator;
1414
15+import charactermanaj.ui.scrollablemenu.JScrollableMenu;
1516 import charactermanaj.util.LocalizedResourcePropertyLoader;
1617
1718 /**
1819 * メニューを構築します.
20+ *
1921 * @author seraphy
2022 */
2123 public class MenuBuilder {
@@ -33,6 +35,7 @@
3335 /**
3436 * メニュー項目のアンチエイリアスが必要か判定する.<br>
3537 * java.specification.versionが1.5で始まる場合は必要とみなす.<br>
38+ *
3639 * @return アンチエイリアスが必要であればtrue
3740 */
3841 private static boolean isNeedAntialias() {
@@ -52,7 +55,9 @@
5255 /**
5356 * 生成されたメニューを名前を指定して取得します.<br>
5457 * 存在しない場合は実行時例外が発生します.<br>
55- * @param name メニュー名
58+ *
59+ * @param name
60+ * メニュー名
5661 * @return メニュー
5762 */
5863 public JMenu getJMenu(String name) {
@@ -66,7 +71,9 @@
6671 /**
6772 * 生成されたメニュー項目を名前を指定して取得します.<br>
6873 * 存在しない場合は実行時例外が発生します.<br>
69- * @param name メニュー項目名
74+ *
75+ * @param name
76+ * メニュー項目名
7077 * @return メニュー項目
7178 */
7279 public JMenuItem getJMenuItem(String name) {
@@ -79,9 +86,11 @@
7986
8087 /**
8188 * メニュー設定に従いメニューバーを構築して返します.<br>
82- * 生成したメニューとメニュー項目は、{@link #getJMenu(String)},
83- * {@link #getJMenuItem(String)}で取得できます.<br>
84- * @param menus メニュー設定
89+ * 生成したメニューとメニュー項目は、{@link #getJMenu(String)}, {@link #getJMenuItem(String)}
90+ * で取得できます.<br>
91+ *
92+ * @param menus
93+ * メニュー設定
8594 * @return 構築されたメニューバー
8695 */
8796 public JMenuBar createMenuBar(MenuDataFactory[] menus) {
@@ -155,6 +164,7 @@
155164 /**
156165 * JMenuBarを構築します.<br>
157166 * アンチエイリアスが必要な場合はアンチエイリアスが設定されます.<br>
167+ *
158168 * @return JMenuBar
159169 */
160170 public JMenuBar createJMenuBar() {
@@ -162,11 +172,7 @@
162172 private static final long serialVersionUID = 1L;
163173 @Override
164174 public void paint(Graphics g) {
165- if (needAntiAlias) {
166- ((Graphics2D) g).setRenderingHint(
167- RenderingHints.KEY_TEXT_ANTIALIASING,
168- RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
169- }
175+ setAntiAlias(g);
170176 super.paint(g);
171177 }
172178 };
@@ -175,26 +181,35 @@
175181 /**
176182 * JMenuを構築します.<br>
177183 * アンチエイリアスが必要な場合はアンチエイリアスが設定されます.<br>
184+ *
178185 * @return JMenu
179186 */
180187 public JMenu createJMenu() {
181- return new JMenu() {
182- private static final long serialVersionUID = 1L;
183- @Override
184- public void paint(Graphics g) {
185- if (needAntiAlias) {
186- ((Graphics2D) g).setRenderingHint(
187- RenderingHints.KEY_TEXT_ANTIALIASING,
188- RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
188+ if (JScrollableMenu.isScreenMenu()) {
189+ return new JMenu() {
190+ private static final long serialVersionUID = 1L;
191+ @Override
192+ public void paint(Graphics g) {
193+ setAntiAlias(g);
194+ super.paint(g);
189195 }
190- super.paint(g);
191- }
192- };
196+ };
197+ } else {
198+ return new JScrollableMenu() {
199+ private static final long serialVersionUID = 1L;
200+ @Override
201+ public void paint(Graphics g) {
202+ setAntiAlias(g);
203+ super.paint(g);
204+ }
205+ };
206+ }
193207 }
194208
195209 /**
196210 * JCheckBoxMenuItemを構築します.<br>
197211 * アンチエイリアスが必要な場合はアンチエイリアスが設定されます.<br>
212+ *
198213 * @return JCheckBoxMenuItem
199214 */
200215 public JCheckBoxMenuItem createJCheckBoxMenuItem() {
@@ -202,11 +217,7 @@
202217 private static final long serialVersionUID = 1L;
203218 @Override
204219 public void paint(Graphics g) {
205- if (needAntiAlias) {
206- ((Graphics2D) g).setRenderingHint(
207- RenderingHints.KEY_TEXT_ANTIALIASING,
208- RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
209- }
220+ setAntiAlias(g);
210221 super.paint(g);
211222 }
212223 };
@@ -215,6 +226,7 @@
215226 /**
216227 * JMenuItemを構築します.<br>
217228 * アンチエイリアスが必要な場合はアンチエイリアスが設定されます.<br>
229+ *
218230 * @return JMenuItem
219231 */
220232 public JMenuItem createJMenuItem() {
@@ -222,14 +234,22 @@
222234 private static final long serialVersionUID = 1L;
223235 @Override
224236 public void paint(Graphics g) {
225- if (needAntiAlias) {
226- ((Graphics2D) g).setRenderingHint(
227- RenderingHints.KEY_TEXT_ANTIALIASING,
228- RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
229- }
237+ setAntiAlias(g);
230238 super.paint(g);
231239 }
232240 };
233241 }
234242
243+ /**
244+ * アンチエイリアスを有効にする.
245+ *
246+ * @param g
247+ */
248+ private static void setAntiAlias(Graphics g) {
249+ if (needAntiAlias) {
250+ ((Graphics2D) g).setRenderingHint(
251+ RenderingHints.KEY_TEXT_ANTIALIASING,
252+ RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
253+ }
254+ }
235255 }
--- trunk/src/charactermanaj/ui/scrollablemenu/ScrollableMenuEventListener.java (nonexistent)
+++ trunk/src/charactermanaj/ui/scrollablemenu/ScrollableMenuEventListener.java (revision 92)
@@ -0,0 +1,27 @@
1+package charactermanaj.ui.scrollablemenu;
2+
3+import java.util.EventListener;
4+
5+/**
6+ * スクローラブルメニューのイベントリスナ
7+ *
8+ * @author seraphy
9+ */
10+public interface ScrollableMenuEventListener extends EventListener {
11+
12+ /**
13+ * スクロール開始を通知する.
14+ *
15+ * @param e
16+ * イベント
17+ */
18+ void start(ScrollableMenuEvent e);
19+
20+ /**
21+ * スクロール終了を通知する.
22+ *
23+ * @param e
24+ * イベント
25+ */
26+ void end(ScrollableMenuEvent e);
27+}
--- trunk/src/charactermanaj/ui/scrollablemenu/JScrollableMenu.java (nonexistent)
+++ trunk/src/charactermanaj/ui/scrollablemenu/JScrollableMenu.java (revision 92)
@@ -0,0 +1,449 @@
1+package charactermanaj.ui.scrollablemenu;
2+
3+import java.awt.event.ActionEvent;
4+import java.awt.event.ActionListener;
5+import java.net.URL;
6+import java.util.ArrayList;
7+import java.util.Collection;
8+
9+import javax.swing.ImageIcon;
10+import javax.swing.JMenu;
11+import javax.swing.JMenuItem;
12+import javax.swing.Timer;
13+import javax.swing.event.MenuEvent;
14+import javax.swing.event.MenuListener;
15+
16+/**
17+ * スクロール可能メニュー. メニュー項目を設定したあと、{@link #initScroller() }でスクローラーを初期化します. つぎに、
18+ * {@link #setScrollableItems(java.util.Collection) }で、スクロールさせる メニュー項目を設定します。
19+ * 表示可能なアイテム数を調整するために、このメニューオブジェクトのselectedイベントの タイミングで、
20+ * {@link #adjustMaxVisible(int) }を呼び出して表示項目数を調整します。
21+ *
22+ * @author seraphy
23+ */
24+public class JScrollableMenu extends JMenu {
25+
26+ /**
27+ * シリアライズバージョンID
28+ */
29+ private static final long serialVersionUID = -5174737355715398136L;
30+
31+ /**
32+ * 自動スクロールの既定の間隔(mSec).
33+ */
34+ public static final int DEFAULT_REPEAT_DELAY = 200;
35+
36+ /**
37+ * 高速自動スクロールの既定の間隔(mSec).
38+ */
39+ public static final int DEFAULT_FAST_REPEAT_DELAY = 80;
40+
41+ /**
42+ * 既定の最大表示アイテム数.
43+ */
44+ public static final int DEFAULT_MAX_VISIBLE = 10;
45+
46+ /**
47+ * リピートの閾値. スクロール数が、この数値を超えた場合に高速スクロール化する.
48+ */
49+ public static final int DEFAULT_REPEAT_THRESHOLD = 3;
50+
51+ /**
52+ * スクロールするアイテムのメニューの開始位置.
53+ */
54+ private int _startPos;
55+
56+ /**
57+ * 現在表示されている最初のアイテムのオフセット.
58+ */
59+ private int _offset;
60+
61+ /**
62+ * スクロールするメニュー項目のリスト.
63+ */
64+ private ArrayList<JMenuItem> _menus = new ArrayList<JMenuItem>();
65+
66+ /**
67+ * 自動スクロールのためのタイマー.
68+ */
69+ private Timer _timer;
70+
71+ /**
72+ * 自動スクロールしたカウント.
73+ */
74+ private int _scrollCount;
75+
76+ /**
77+ * スクローラー(上).
78+ */
79+ private JScrollerMenuItem _upButton;
80+
81+ /**
82+ * スクローラー(下).
83+ */
84+ private JScrollerMenuItem _downButton;
85+
86+ /**
87+ * 通常スクロール時の自動スクロールの間隔.
88+ */
89+ private int _delay = DEFAULT_REPEAT_DELAY;
90+
91+ /**
92+ * 高速スクロール時の自動スクロールの間隔.
93+ */
94+ private int _delayFast = DEFAULT_FAST_REPEAT_DELAY;
95+
96+ /**
97+ * リピートの閾値. スクロール数が、この数値を超えた場合に高速スクロール化する.
98+ */
99+ private int _repeat_threshold = DEFAULT_REPEAT_THRESHOLD;
100+
101+ /**
102+ * 現在のスクロール方向を示すフラグ. タイマーハンドラの中で判定するため. nullの場合はスクロールしていないを示す.
103+ */
104+ private Boolean _directionUp;
105+
106+ /**
107+ * 最大表示アイテム数.
108+ */
109+ private int maxVisible = DEFAULT_MAX_VISIBLE;
110+
111+ /**
112+ * 表示名を省略してメニューを構築する.
113+ */
114+ public JScrollableMenu() {
115+ this("");
116+ }
117+
118+ /**
119+ * 表示名を指定してメニューを構築する.
120+ *
121+ * @param name
122+ */
123+ public JScrollableMenu(String name) {
124+ super(name);
125+ initScrollableMenu();
126+ }
127+
128+ /**
129+ * スクロール可能メニューの基本状態を設定する.
130+ */
131+ private void initScrollableMenu() {
132+ // 自動スクロールのためのタイマ
133+ this._timer = new Timer(_delay, new ActionListener() {
134+ public void actionPerformed(ActionEvent e) {
135+ // スクロール
136+ doScroll();
137+
138+ // スクロール数をカウントアップ
139+ _scrollCount++;
140+
141+ // スクロール数が閾値を超えたら高速化
142+ if (_scrollCount >= _repeat_threshold) {
143+ ((Timer) e.getSource()).setDelay(_delayFast);
144+ }
145+ }
146+ });
147+ addMenuListener(new MenuListener() {
148+ public void menuCanceled(MenuEvent e) {
149+ // このメニューがキャンセルされたときにスクロールを停止する
150+ JScrollableMenu.this._timer.stop();
151+ _directionUp = null;
152+ }
153+
154+ public void menuDeselected(MenuEvent e) {
155+ // このメニューが非選択状態になったときスクロールを停止する
156+ JScrollableMenu.this._timer.stop();
157+ _directionUp = null;
158+ }
159+
160+ public void menuSelected(MenuEvent e) {
161+ // 何もしない
162+ }
163+ });
164+ }
165+
166+ /**
167+ * スクローラーを初期化します. スクロールしない固定のメニュー項目などを設定したあとで、このメソッドを呼び出します.
168+ * すでに初期化されている場合は何もしません.
169+ */
170+ public void initScroller() {
171+ if (_upButton != null || _downButton != null) {
172+ // すでに初期化済み
173+ removeAllScrollableItems();
174+ return;
175+ }
176+
177+ // スクローラー用ボタンアイコンを、このクラスからの相対パスで取得する.
178+ // (派生クラスからでもリソースの相対位置を変えないようにするためクラス名は固定とする)
179+ Class<?> cls = JScrollableMenu.class;
180+ URL downPngURL = cls.getResource("arrow-down.png");
181+ URL upPngURL = cls.getResource("arrow-up.png");
182+ if (downPngURL == null || upPngURL == null) {
183+ throw new RuntimeException("png resource not found.");
184+ }
185+ ImageIcon iconDown = new ImageIcon(downPngURL);
186+ ImageIcon iconUp = new ImageIcon(upPngURL);
187+
188+ // スクローラー用メニュー項目
189+ _upButton = new JScrollerMenuItem(iconUp);
190+ _downButton = new JScrollerMenuItem(iconDown);
191+
192+ // スクローラーのマウスイベントを受け取る
193+ final ScrollableMenuEventListener sc = new ScrollableMenuEventListener() {
194+ public void start(ScrollableMenuEvent e) {
195+ Boolean direction;
196+ if (e.getSource().equals(_upButton)) {
197+ // 上スクロール
198+ direction = Boolean.TRUE;
199+ } else {
200+ // 下スクロール
201+ direction = Boolean.FALSE;
202+ }
203+
204+ // マウスクリックに対するスクロール
205+ doScroll(direction);
206+
207+ // 自動スクロール開始
208+ _scrollCount = 0;
209+ _timer.setDelay(_delay);
210+ _timer.start();
211+ }
212+
213+ public void end(ScrollableMenuEvent e) {
214+ // 自動スクロール停止
215+ _timer.stop();
216+ _directionUp = null;
217+ }
218+ };
219+
220+ _upButton.addScrollableMenuEventListener(sc);
221+ _downButton.addScrollableMenuEventListener(sc);
222+
223+ add(_upButton);
224+ _startPos = getItemCount(); // upButtonの次のインデックス
225+ add(_downButton);
226+
227+ // Mac OS Xのスクリーンメニューはスクロール可能なので、
228+ // スクローラー用アイテムは非表示にして、デフォルトの機能に任せる。
229+ // (逆に、スクリーンメニューではカスタムメニューは、うまく機能しない。)
230+ if (isScreenMenu()) {
231+ _upButton.setVisible(false);
232+ _downButton.setVisible(false);
233+ }
234+ }
235+
236+ /**
237+ * 1行スクロールする
238+ *
239+ * @param direction
240+ * 上方向の場合はtrue、下の場合はfalse、停止はnull
241+ */
242+ public void doScroll(Boolean direction) {
243+ _directionUp = direction;
244+ doScroll();
245+ }
246+
247+ /**
248+ * スクロールする.
249+ */
250+ protected void doScroll() {
251+ // 現在の方向に応じて処理内容を分岐する.
252+ if (_directionUp != null) {
253+ if (_directionUp.booleanValue()) {
254+ scrollDown();
255+ } else {
256+ scrollUp();
257+ }
258+ }
259+ }
260+
261+ /**
262+ * Mac OS Xのスクリーンメニューを使用しているか?
263+ *
264+ * @return 使用している場合はtrue
265+ */
266+ public static boolean isScreenMenu() {
267+ String macScreenMenu = System.getProperty("apple.laf.useScreenMenuBar");
268+ if (macScreenMenu != null && macScreenMenu.toLowerCase().equals("true")) {
269+ return true;
270+ }
271+ return false;
272+ }
273+
274+ /**
275+ * 表示可能な最大行数を設定する.
276+ *
277+ * @param maxVisible
278+ * 最大行数
279+ */
280+ public void setMaxVisible(int maxVisible) {
281+ this.maxVisible = maxVisible;
282+ }
283+
284+ /**
285+ * 表示可能な最大行数を取得する.
286+ *
287+ * @return 表示可能な最大行数
288+ */
289+ public int getMaxVisible() {
290+ return this.maxVisible;
291+ }
292+
293+ /**
294+ * 画面の高さを指定して、表示可能なスクロールのアイテム数を算定し、 スクロールを表示し直す.
295+ *
296+ * @param height
297+ * 画面の高さを示す(px)
298+ */
299+ public void adjustMaxVisible(int height) {
300+ int numOfItems = 0;
301+ if (_menus.size() > 0) {
302+ int heightPerItem = _menus.get(0).getPreferredSize().height;
303+ if (heightPerItem <= 0) {
304+ // 調整できないので何もしない.
305+ return;
306+ }
307+ numOfItems = height / heightPerItem;
308+ }
309+ numOfItems = numOfItems - (_startPos + 1 + 2); // 既存 + up/downボタン分 +
310+ // 上下余白を差し引く
311+ if (numOfItems < 0) {
312+ numOfItems = 1;
313+ }
314+ this.maxVisible = numOfItems;
315+ updateScrollableMenus();
316+ }
317+
318+ /**
319+ * 通常スクロールの間隔を取得する.
320+ *
321+ * @return 通常スクロールの間隔(mSec)
322+ */
323+ public int getRepeatDelay() {
324+ return this._delay;
325+ }
326+
327+ /**
328+ * 高速スクロールの間隔を取得する.
329+ *
330+ * @return 高速スクロールの間隔(mSec)
331+ */
332+ public int getRepeatDelayFast() {
333+ return this._delayFast;
334+ }
335+
336+ /**
337+ * 通常スクロールの間隔を設定する.
338+ *
339+ * @param delay
340+ * 通常スクロールの間隔(mSec)
341+ */
342+ public void setRepeatDelay(int delay) {
343+ this._delay = delay;
344+ }
345+
346+ /**
347+ * 高速スクロールの間隔を設定する.
348+ *
349+ * @param delayFast
350+ * 高速スクロールの間隔(mSec)
351+ */
352+ public void setRepeatDelayFast(int delayFast) {
353+ this._delayFast = delayFast;
354+ }
355+
356+ /**
357+ * スクロール可能アイテムを設定します. 既存のアイテムがある場合は、すべて登録解除されます. 事前にスクローラーは初期化済みでなければなりません.
358+ *
359+ * @param menus
360+ * メニューリスト
361+ */
362+ public void setScrollableItems(Collection<? extends JMenuItem> menus) {
363+ if (_upButton == null || _downButton == null) {
364+ throw new IllegalStateException("initScrollerを先に呼び出してください");
365+ }
366+ removeAllScrollableItems();
367+
368+ if (menus != null) {
369+ for (JMenuItem item : menus) {
370+ int idx = _startPos + _menus.size();
371+ this.add(item, idx);
372+ _menus.add(item);
373+ }
374+ }
375+
376+ updateScrollableMenus();
377+ }
378+
379+ /**
380+ * 現在のスクロール可能アイテムをすべて除去します.
381+ */
382+ public void removeAllScrollableItems() {
383+ for (JMenuItem item : _menus) {
384+ this.remove(item);
385+ }
386+ _menus.clear();
387+ _offset = 0;
388+ }
389+
390+ /**
391+ * 現在のスクロール範囲でスクロール可能項目を表示します.
392+ */
393+ public void updateScrollableMenus() {
394+ boolean screenMenu = isScreenMenu();
395+ int numOfItems = _menus.size();
396+ for (int idx = 0; idx < numOfItems; idx++) {
397+ boolean visible = false;
398+ if (idx >= _offset && idx < (_offset + maxVisible) || screenMenu) {
399+ // メニュー項目が表示範囲内であれば表示、範囲外であれび非表示とする。
400+ // ただし、Mac OS Xのスクリーンメニューであれば無条件にすべて表示。
401+ visible = true;
402+ }
403+ _menus.get(idx).setVisible(visible);
404+ }
405+ }
406+
407+ /**
408+ * 現在表示されているスクロール項目のオフセットを取得する.
409+ *
410+ * @return 現在のオフセット
411+ */
412+ public int getOffset() {
413+ return _offset;
414+ }
415+
416+ /**
417+ * 上方向にスクロールします. これ以上スクロールできない場合は何もしません. その場合、自動スクロール中であればスクロールは停止します.
418+ */
419+ public void scrollUp() {
420+ int numOfItems = _menus.size();
421+ int limit = numOfItems - maxVisible;
422+ if (limit < 0) {
423+ limit = 0;
424+ }
425+
426+ _offset++;
427+
428+ if (_offset >= limit) {
429+ _offset = limit;
430+ _timer.stop();
431+ _directionUp = null;
432+ }
433+
434+ updateScrollableMenus();
435+ }
436+
437+ /**
438+ * 下方向にスクロールします. これ以上スクロールできない場合は何もしません。 その場合、自動スクロール中であればスクロールは停止します。
439+ */
440+ public void scrollDown() {
441+ _offset--;
442+ if (_offset < 0) {
443+ _offset = 0;
444+ _timer.stop();
445+ _directionUp = null;
446+ }
447+ updateScrollableMenus();
448+ }
449+}
--- trunk/src/charactermanaj/ui/scrollablemenu/JScrollerMenuItem.java (nonexistent)
+++ trunk/src/charactermanaj/ui/scrollablemenu/JScrollerMenuItem.java (revision 92)
@@ -0,0 +1,93 @@
1+package charactermanaj.ui.scrollablemenu;
2+
3+import java.awt.event.MouseEvent;
4+
5+import javax.swing.Icon;
6+import javax.swing.JMenuItem;
7+import javax.swing.event.EventListenerList;
8+
9+/**
10+ * スクローラブルメニューのスクローラーアイテムのメニュー項目
11+ *
12+ * @author seraphy
13+ */
14+public class JScrollerMenuItem extends JMenuItem {
15+
16+ /**
17+ * シリアライズバージョンID
18+ */
19+ private static final long serialVersionUID = -1749741596476938310L;
20+ /**
21+ * イベントリスナのコレクション
22+ */
23+ protected EventListenerList _listeners = new EventListenerList();
24+
25+ /**
26+ * スクローラーのアイコンを指定してスクローラーアイテムのメニュー項目を構築します.
27+ *
28+ * @param icon
29+ * アイコン
30+ */
31+ public JScrollerMenuItem(Icon icon) {
32+ setIcon(icon);
33+ }
34+
35+ /**
36+ * スクローラブルメニューイベントのイベントリスナを登録します.
37+ *
38+ * @param l
39+ * リスナー
40+ */
41+ public void addScrollableMenuEventListener(ScrollableMenuEventListener l) {
42+ _listeners.add(ScrollableMenuEventListener.class, l);
43+ }
44+
45+ /**
46+ * スクローラブルメニューイベントのイベントリスナを登録解除します.
47+ *
48+ * @param l
49+ * リスナー
50+ */
51+ public void removeScrollableMenuEventListener(ScrollableMenuEventListener l) {
52+ _listeners.remove(ScrollableMenuEventListener.class, l);
53+ }
54+
55+ /**
56+ * マウスクリックでメニューアイテムとしてのイベントが発生しないように、 マウスイベントをキャプチャして、スクローラブルメニューイベントに変換する。
57+ *
58+ * @param e
59+ */
60+ @Override
61+ protected void processMouseEvent(MouseEvent e) {
62+ ScrollableMenuEvent ee = null;
63+ int mouseEventId = e.getID();
64+ if (mouseEventId == MouseEvent.MOUSE_PRESSED) {
65+ // マウスダウン時、スクロール開始
66+ ee = new ScrollableMenuEvent(this, true);
67+ }
68+ if (mouseEventId == MouseEvent.MOUSE_RELEASED) {
69+ // マウスアップされた場合、スクロール停止
70+ ee = new ScrollableMenuEvent(this, false);
71+ }
72+ if (ee != null) {
73+ fireScrollableMenuEvent(ee);
74+ }
75+ }
76+
77+ /**
78+ * スクローラブルメニューイベントを送信する
79+ *
80+ * @param e
81+ * メニューイベント
82+ */
83+ protected void fireScrollableMenuEvent(ScrollableMenuEvent e) {
84+ for (ScrollableMenuEventListener l : _listeners
85+ .getListeners(ScrollableMenuEventListener.class)) {
86+ if (e.isScrolling()) {
87+ l.start(e);
88+ } else {
89+ l.end(e);
90+ }
91+ }
92+ }
93+}
--- trunk/src/charactermanaj/ui/scrollablemenu/ScrollableMenuEvent.java (nonexistent)
+++ trunk/src/charactermanaj/ui/scrollablemenu/ScrollableMenuEvent.java (revision 92)
@@ -0,0 +1,59 @@
1+package charactermanaj.ui.scrollablemenu;
2+
3+import java.util.EventObject;
4+
5+/**
6+ * スクローラブルメニューのイベント
7+ *
8+ * @author seraphy
9+ */
10+public class ScrollableMenuEvent extends EventObject {
11+
12+ /**
13+ * シリアライズバージョンID
14+ */
15+ private static final long serialVersionUID = 5686533260565824649L;
16+
17+ /**
18+ * スクロール中フラグ
19+ */
20+ private boolean _scrolling;
21+
22+ /**
23+ * イベントのコンストラクタ
24+ *
25+ * @param s
26+ * イベントソース
27+ * @param scrolling
28+ * スクロール中フラグ
29+ */
30+ public ScrollableMenuEvent(JScrollerMenuItem s, boolean scrolling) {
31+ super(s);
32+ this._scrolling = scrolling;
33+ }
34+
35+ /**
36+ * スクロール中か?
37+ *
38+ * @return スクロール中であればtrue
39+ */
40+ public boolean isScrolling() {
41+ return _scrolling;
42+ }
43+
44+ /**
45+ * 診断用
46+ *
47+ * @return 診断用文字列
48+ */
49+ @Override
50+ public String toString() {
51+ StringBuilder buf = new StringBuilder();
52+ buf.append(getClass().getSimpleName());
53+ buf.append("[");
54+ buf.append(this.source);
55+ buf.append(",scrolling=").append(this._scrolling);
56+ buf.append("]");
57+ return buf.toString();
58+ }
59+}
--- trunk/src/charactermanaj/ui/SelectCharatersDirDialog.java (revision 91)
+++ trunk/src/charactermanaj/ui/SelectCharatersDirDialog.java (revision 92)
@@ -33,11 +33,13 @@
3333 import javax.swing.JFileChooser;
3434 import javax.swing.JFrame;
3535 import javax.swing.JLabel;
36+import javax.swing.JOptionPane;
3637 import javax.swing.JPanel;
3738 import javax.swing.JRootPane;
3839 import javax.swing.KeyStroke;
3940
4041 import charactermanaj.Main;
42+import charactermanaj.model.io.WorkingSetPersist;
4143 import charactermanaj.ui.util.FileDropTarget;
4244 import charactermanaj.util.ErrorMessageHelper;
4345 import charactermanaj.util.LocalizedResourcePropertyLoader;
@@ -171,6 +173,15 @@
171173 }
172174 };
173175
176+ AbstractAction actRemoveWorkingSets = new AbstractAction(
177+ strings.getProperty("btn.clearWorkingSets")) {
178+ private static final long serialVersionUID = 1L;
179+ public void actionPerformed(ActionEvent e) {
180+ onRemoveWorkingSets();
181+ }
182+ };
183+
184+ final JButton btnRemoveWorkingSets = new JButton(actRemoveWorkingSets);
174185 final JButton btnRemoveRecent = new JButton(actRemoveRecent);
175186 final JButton btnOK = new JButton(actOk);
176187 final JButton btnCancel = new JButton(actClose);
@@ -195,6 +206,7 @@
195206 im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, tk.getMenuShortcutKeyMask()), "close");
196207 rootPane.getActionMap().put("close", actClose);
197208
209+ btnRemoveWorkingSets.addFocusListener(focusAdapter);
198210 btnRemoveRecent.addFocusListener(focusAdapter);
199211 btnOK.addFocusListener(focusAdapter);
200212 btnCancel.addFocusListener(focusAdapter);
@@ -256,12 +268,20 @@
256268 gbc.gridy = 1;
257269 gbc.gridwidth = 1;
258270 gbc.gridheight = 1;
271+ gbc.weightx = 0.;
272+ gbc.weighty = 0.;
273+ btnPanel.add(btnRemoveWorkingSets, gbc);
274+
275+ gbc.gridx = 2;
276+ gbc.gridy = 1;
277+ gbc.gridwidth = 1;
278+ gbc.gridheight = 1;
259279 gbc.weightx = 1.;
260280 gbc.weighty = 0.;
261281
262282 btnPanel.add(Box.createGlue(), gbc);
263283
264- gbc.gridx = Main.isLinuxOrMacOSX() ? 3 : 2;
284+ gbc.gridx = Main.isLinuxOrMacOSX() ? 4 : 3;
265285 gbc.gridy = 1;
266286 gbc.gridwidth = 1;
267287 gbc.gridheight = 1;
@@ -271,7 +291,7 @@
271291 btnPanel.add(btnOK, gbc);
272292
273293
274- gbc.gridx = Main.isLinuxOrMacOSX() ? 2 : 3;
294+ gbc.gridx = Main.isLinuxOrMacOSX() ? 3 : 4;
275295 gbc.gridy = 1;
276296 gbc.gridwidth = 1;
277297 gbc.gridheight = 1;
@@ -279,7 +299,7 @@
279299 gbc.weighty = 0.;
280300 btnPanel.add(btnCancel, gbc);
281301
282- gbc.gridx = 4;
302+ gbc.gridx = 5;
283303 gbc.gridy = 1;
284304 gbc.gridwidth = 1;
285305 gbc.gridheight = 1;
@@ -389,6 +409,30 @@
389409 ErrorMessageHelper.showErrorDialog(this, ex);
390410 }
391411 }
412+
413+ protected void onRemoveWorkingSets() {
414+ try {
415+ Properties strings = LocalizedResourcePropertyLoader
416+ .getCachedInstance().getLocalizedProperties(
417+ "languages/selectCharatersDirDialog");
418+
419+ // 削除の確認ダイアログ
420+ if (JOptionPane.showConfirmDialog(this,
421+ strings.getProperty("confirm.clearWorkingSets"),
422+ strings.getProperty("confirm.clearWorkingSets.title"),
423+ JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION) {
424+ return;
425+ }
426+
427+ // 全てのワーキングセットをクリアする.
428+ WorkingSetPersist workingSetPersist = WorkingSetPersist
429+ .getInstance();
430+ workingSetPersist.removeAllWorkingSet();
431+
432+ } catch (Exception ex) {
433+ ErrorMessageHelper.showErrorDialog(this, ex);
434+ }
435+ }
392436
393437 protected void onRemoveRecent() {
394438 try {
--- trunk/src/charactermanaj/ui/util/WindowAdjustLocationSupport.java (nonexistent)
+++ trunk/src/charactermanaj/ui/util/WindowAdjustLocationSupport.java (revision 92)
@@ -0,0 +1,70 @@
1+package charactermanaj.ui.util;
2+
3+import java.awt.Dimension;
4+import java.awt.GraphicsEnvironment;
5+import java.awt.Insets;
6+import java.awt.Point;
7+import java.awt.Rectangle;
8+import java.awt.Window;
9+
10+import javax.swing.JFrame;
11+
12+/**
13+ * ウィンドウの位置を調整するサポートクラス.<br>
14+ *
15+ * @author seraphy
16+ */
17+public final class WindowAdjustLocationSupport {
18+
19+ /**
20+ * プライベートコンストラクタ
21+ */
22+ private WindowAdjustLocationSupport() {
23+ super();
24+ }
25+
26+ /**
27+ * ウィンドウの表示位置をメインウィンドウの右側に調整する.<br>
28+ * 横位置Xはメインフレームの右側とし、縦位置Yはメインフレームの上位置からのoffset_yを加えた位置とする.<br>
29+ *
30+ * @param mainWindow
31+ * 基準位置となるメインウィンドウ
32+ * @param window
33+ * 位置を調整するウィンドウ
34+ * @param offset_y
35+ * 表示のYオフセット
36+ * @param sameHeight
37+ * 高さをメインウィンドウにそろえるか?
38+ */
39+ public static void alignRight(JFrame mainWindow, Window window,
40+ int offset_y, boolean sameHeight) {
41+ // メインウィンドウよりも左側に位置づけする.
42+ // 縦位置はメインウィンドウの上端からオフセットを加えたものとする.
43+ Point pt = mainWindow.getLocation();
44+ Insets insets = mainWindow.getInsets();
45+ pt.x += mainWindow.getWidth();
46+ pt.y += (offset_y * insets.top);
47+
48+ // メインスクリーンサイズを取得する.
49+ GraphicsEnvironment genv = GraphicsEnvironment
50+ .getLocalGraphicsEnvironment();
51+ Rectangle desktopSize = genv.getMaximumWindowBounds(); // メインスクリーンのサイズ(デスクトップ領域のみ)
52+
53+ // メインスクリーンサイズを超えた場合は、はみ出た分を移動する.
54+ if ((pt.x + window.getWidth()) > desktopSize.width) {
55+ pt.x -= ((pt.x + window.getWidth()) - desktopSize.width);
56+ }
57+ if ((pt.y + window.getHeight()) > desktopSize.height) {
58+ pt.y -= ((pt.y + window.getHeight()) - desktopSize.height);
59+ }
60+
61+ window.setLocation(pt);
62+
63+ // 高さはメインフレームと同じにする.
64+ if (sameHeight) {
65+ Dimension siz = window.getSize();
66+ siz.height = mainWindow.getHeight() - offset_y;
67+ window.setSize(siz);
68+ }
69+ }
70+}
--- trunk/src/charactermanaj/ui/ColorDialog.java (revision 91)
+++ trunk/src/charactermanaj/ui/ColorDialog.java (revision 92)
@@ -2,13 +2,10 @@
22
33 import java.awt.BorderLayout;
44 import java.awt.Container;
5-import java.awt.GraphicsEnvironment;
65 import java.awt.GridBagConstraints;
76 import java.awt.GridBagLayout;
87 import java.awt.GridLayout;
98 import java.awt.Insets;
10-import java.awt.Point;
11-import java.awt.Rectangle;
129 import java.awt.Toolkit;
1310 import java.awt.event.ActionEvent;
1411 import java.awt.event.ActionListener;
@@ -53,6 +50,7 @@
5350 import javax.swing.event.ChangeEvent;
5451 import javax.swing.event.ChangeListener;
5552
53+import charactermanaj.Main;
5654 import charactermanaj.graphics.colormodel.ColorModel;
5755 import charactermanaj.graphics.colormodel.ColorModels;
5856 import charactermanaj.graphics.filters.ColorConv;
@@ -71,6 +69,7 @@
7169 /**
7270 * カラーダイアログ.<br>
7371 * カラーダイアログはカテゴリ別に関連づけられており、カテゴリ内の各レイヤーに対応するタブを持つ.<br>
72+ *
7473 * @author seraphy
7574 */
7675 public class ColorDialog extends JDialog {
@@ -130,9 +129,13 @@
130129
131130 /**
132131 * コンストラクタ
133- * @param parent 親フレーム
134- * @param partsCategory カテゴリ
135- * @param colorGroups 選択可能なカラーグループのコレクション
132+ *
133+ * @param parent
134+ * 親フレーム
135+ * @param partsCategory
136+ * カテゴリ
137+ * @param colorGroups
138+ * 選択可能なカラーグループのコレクション
136139 */
137140 public ColorDialog(JFrame parent, PartsCategory partsCategory, Collection<ColorGroup> colorGroups) {
138141 super(parent);
@@ -200,7 +203,7 @@
200203 });
201204 tabContainer.addPropertyChangeListener("colorConvertParameter", new PropertyChangeListener() {
202205 public void propertyChange(PropertyChangeEvent evt) {
203- // レイヤーの情報が変るたびにリセットボタンの状態を更新する
206+ // レイヤーの情報が変るたびにリセットボタンの状態を更新する
204207 updateResetButton(tabContainer);
205208 }
206209 });
@@ -304,10 +307,11 @@
304307 }
305308
306309 /**
307- * 各レイヤーのカラー情報のタブが開かれた場合、もしくはカラーの設定値が変更されるたびに
308- * 呼び出されて、リセットボタンの状態を変更します.<br>
310+ * 各レイヤーのカラー情報のタブが開かれた場合、もしくはカラーの設定値が変更されるたびに 呼び出されて、リセットボタンの状態を変更します.<br>
309311 * 現在のタブが選択しているパネルと異なるパネルからの要求については無視されます.<br>
310- * @param panel 色情報が変更された、もしくは開かれた対象パネル
312+ *
313+ * @param panel
314+ * 色情報が変更された、もしくは開かれた対象パネル
311315 */
312316 protected void updateResetButton(ColorDialogTabPanel panel) {
313317 ColorDialogTabPanel currentPanel = (ColorDialogTabPanel) tabbedPane.getSelectedComponent();
@@ -317,35 +321,8 @@
317321 }
318322
319323 /**
320- * ダイアログの表示位置を調整する.<br>
321- * 横位置Xはメインフレームの右側とし、縦位置Yはメインフレームの上位置からのoffset_yを加えた位置とする.<br>
322- * @param offset_y オフセットY
323- */
324- public void adjustLocation(int offset_y) {
325- // メインウィンドウよりも左側に位置づけする.
326- // 縦位置はメインウィンドウの上端からオフセットを加えたものとする.
327- Point pt = getParent().getLocation();
328- Insets insets = getParent().getInsets();
329- pt.x += getParent().getWidth();
330- pt.y += (offset_y * insets.top);
331-
332- // メインスクリーンサイズを取得する.
333- GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
334- Rectangle desktopSize = genv.getMaximumWindowBounds(); // メインスクリーンのサイズ(デスクトップ領域のみ)
335-
336- // メインスクリーンサイズを超えた場合は、はみ出た分を移動する.
337- if ((pt.x + getWidth()) > desktopSize.width) {
338- pt.x -= ((pt.x + getWidth()) - desktopSize.width);
339- }
340- if ((pt.y + getHeight()) > desktopSize.height) {
341- pt.y -= ((pt.y + getHeight()) - desktopSize.height);
342- }
343-
344- setLocation(pt);
345- }
346-
347- /**
348324 * このカラーダイアログが対応するパーツカテゴリを取得する.<br>
325+ *
349326 * @return パーツカテゴリ
350327 */
351328 public PartsCategory getPartsCategory() {
@@ -355,7 +332,9 @@
355332 /**
356333 * 指定したレイヤーのカラーグループが「連動」しているか?<br>
357334 * カテゴリに属していないレイヤーを指定した場合は常にfalseを返す.<br>
358- * @param layer レイヤー
335+ *
336+ * @param layer
337+ * レイヤー
359338 * @return 連動している場合はtrue、そうでなければfalse
360339 */
361340 public boolean isSyncColorGroup(Layer layer) {
@@ -369,8 +348,11 @@
369348 /**
370349 * 指定したレイヤーのカラーグループの連動フラグを設定する.<br>
371350 * カテゴリに属していないレイヤーを指定した場合は何もしない.<br>
372- * @param layer レイヤー
373- * @param selected 連動フラグ
351+ *
352+ * @param layer
353+ * レイヤー
354+ * @param selected
355+ * 連動フラグ
374356 */
375357 public void setSyncColorGroup(Layer layer, boolean selected) {
376358 ColorDialogTabPanel tab = tabs.get(layer);
@@ -382,7 +364,9 @@
382364 /**
383365 * レイヤーごとの色情報のマップを指定して、各レイヤーに色情報を設定する.<br>
384366 * カテゴリに属していないレイヤーが含まれる場合は例外となる.<br>
385- * @param params レイヤーと色情報のマップ
367+ *
368+ * @param params
369+ * レイヤーと色情報のマップ
386370 */
387371 public void setColorConvertParameters(Map<Layer, ColorConvertParameter> params) {
388372 if (params == null) {
@@ -401,7 +385,9 @@
401385 * 対象となるパーツ識別子を指定する。<br>
402386 * カラーダイアログのキャプションにパーツ名を設定される.<br>
403387 * nullを指定した場合はキャプションからパーツ名が除去される.<br>
404- * @param partsIdentifier パーツ識別子、もしくはnull
388+ *
389+ * @param partsIdentifier
390+ * パーツ識別子、もしくはnull
405391 */
406392 public void setPartsIdentifier(PartsIdentifier partsIdentifier) {
407393 this.partsIdentifier = partsIdentifier;
@@ -415,6 +401,7 @@
415401 /**
416402 * 対象となるパーツ識別子を取得する.<br>
417403 * 設定されていなければnullが返される.<br>
404+ *
418405 * @return パーツ識別子、もしくはnull
419406 */
420407 public PartsIdentifier getPartsIdentifier() {
@@ -425,7 +412,9 @@
425412 * 各レイヤーのタブの有効・無効状態を設定します.<br>
426413 * カテゴリに属さないレイヤーは無視されます.<br>
427414 * nullを指定した場合は、すべてのレイヤーが「有効」となります.<br>
428- * @param layers 有効とするレイヤーのコレクション、もしくはnull
415+ *
416+ * @param layers
417+ * 有効とするレイヤーのコレクション、もしくはnull
429418 */
430419 public void setEnableLayers(Collection<Layer> layers) {
431420 for (Map.Entry<Layer, ColorDialogTabPanel> entry : tabs.entrySet()) {
@@ -433,8 +422,16 @@
433422 boolean enabled = (layers == null) || layers.contains(layer);
434423 Integer tabIndex = tabbedPaneIndexMap.get(layer);
435424 if (tabIndex != null) {
425+ if (Main.isMacOSX()) {
426+ // OSXの場合、タブをディセーブルにしても表示が変化ないので
427+ // タブタイトルを変更することでディセーブルを示す.
428+ // (html3で表現しようとしたところ、かなりバギーだったため採用せず)
429+ tabbedPane.setTitleAt(tabIndex,
430+ enabled ? layer.getLocalizedName() : "-");
431+ }
432+
436433 tabbedPane.setEnabledAt(tabIndex, enabled);
437-
434+
438435 if (logger.isLoggable(Level.FINEST)) {
439436 logger.log(Level.FINEST, "setEnableLayers(" + layer + ")=" + enabled);
440437 }
@@ -444,6 +441,7 @@
444441
445442 /**
446443 * 「すべてに適用」フラグを取得する.<br>
444+ *
447445 * @return すべてに適用フラグ
448446 */
449447 public boolean isApplyAll() {
@@ -452,6 +450,7 @@
452450
453451 /**
454452 * 各レイヤーと、その色情報をマップとして取得する.<br>
453+ *
455454 * @return 各レイヤーと、その色情報のマップ
456455 */
457456 public Map<Layer, ColorConvertParameter> getColorConvertParameters() {
@@ -467,8 +466,11 @@
467466 /**
468467 * レイヤーを指定して、色情報を設定する.<br>
469468 * カテゴリに属していないレイヤーを指定した場合は例外となる.<br>
470- * @param layer レイヤー
471- * @param param 色情報
469+ *
470+ * @param layer
471+ * レイヤー
472+ * @param param
473+ * 色情報
472474 */
473475 public void setColorConvertParameter(Layer layer, ColorConvertParameter param) {
474476 if (layer == null || param == null) {
@@ -484,7 +486,9 @@
484486 /**
485487 * 指定したレイヤーの色情報を取得する.<br>
486488 * カテゴリに属していないレイヤーを指定した場合は例外となる.<br>
487- * @param layer レイヤー
489+ *
490+ * @param layer
491+ * レイヤー
488492 * @return 色情報
489493 */
490494 public ColorConvertParameter getColorConvertParameter(Layer layer) {
@@ -501,7 +505,9 @@
501505 /**
502506 * 指定したレイヤーのカラーグループを取得する.<br>
503507 * カテゴリに属さないレイヤーを指定した場合は例外となる.<br>
504- * @param layer レイヤー
508+ *
509+ * @param layer
510+ * レイヤー
505511 * @return カラーグループ
506512 */
507513 public ColorGroup getColorGroup(Layer layer) {
@@ -518,8 +524,11 @@
518524 /**
519525 * 指定したレイヤーのカラーグループを設定する.<br>
520526 * カテゴリに属さないレイヤーを指定した場合は例外となる.<br>
521- * @param layer レイヤー
522- * @param colorGroup カラーグループ
527+ *
528+ * @param layer
529+ * レイヤー
530+ * @param colorGroup
531+ * カラーグループ
523532 */
524533 public void setColorGroup(Layer layer, ColorGroup colorGroup) {
525534 if (layer == null || colorGroup == null) {
@@ -533,7 +542,9 @@
533542
534543 /**
535544 * 色ダイアログが変更された場合に通知を受けるリスナーを登録する.<br>
536- * @param listener リスナー
545+ *
546+ * @param listener
547+ * リスナー
537548 */
538549 public void addColorChangeListener(ColorChangeListener listener) {
539550 if (listener == null) {
@@ -544,6 +555,7 @@
544555
545556 /**
546557 * 色ダイアログが変更された場合に通知を受けるリスナーを登録解除する.<br>
558+ *
547559 * @param listener
548560 */
549561 public void removeColorChangeListener(ColorChangeListener listener) {
@@ -569,8 +581,11 @@
569581 /**
570582 * 指定したレイヤーに対するカラー変更イベントを通知する.<br>
571583 * ただし、force引数がfalseである場合、アプリケーション設定で即時プレビューが指定されていない場合は通知しない.<br>
572- * @param layer レイヤー
573- * @param force アプリケーション設定に関わらず送信する場合はtrue
584+ *
585+ * @param layer
586+ * レイヤー
587+ * @param force
588+ * アプリケーション設定に関わらず送信する場合はtrue
574589 */
575590 protected void fireColorChangeEvent(Layer layer, boolean force) {
576591 if (layer == null) {
@@ -590,7 +605,9 @@
590605
591606 /**
592607 * 色グループが変更されたことを通知する.<br>
593- * @param layer レイヤー
608+ *
609+ * @param layer
610+ * レイヤー
594611 */
595612 protected void fireColorGroupChangeEvent(Layer layer) {
596613 if (layer == null) {
@@ -887,7 +904,8 @@
887904 .getItemTitle(1)), JLabel.CENTER)); // Saturation 彩度
888905 colorTunePanel.add(new JLabel(strings.getProperty(colorModel
889906 .getItemTitle(2)), JLabel.CENTER)); // Brightness 明度
890- colorTunePanel.add(new JLabel(strings.getProperty("contrast"), JLabel.CENTER)); // Contrast コントラスト
907+ colorTunePanel.add(new JLabel(strings.getProperty("contrast"),
908+ JLabel.CENTER)); // Contrast コントラスト
891909
892910 SpinnerNumberModel hsbModelH = new SpinnerNumberModel(0., -1., 1., 0.001);
893911 SpinnerNumberModel hsbModelS = new SpinnerNumberModel(0., -1., 1., 0.001);
@@ -1027,8 +1045,7 @@
10271045
10281046 /**
10291047 * このパネルで変更された色情報の状態をリセットする.<br>
1030- * 最後に{@link #setColorConvertParameter(ColorConvertParameter)}された値で
1031- * 設定し直す.<br>
1048+ * 最後に{@link #setColorConvertParameter(ColorConvertParameter)}された値で 設定し直す.<br>
10321049 */
10331050 public void resetColor() {
10341051 setColorConvertParameter(paramOrg);
@@ -1102,6 +1119,7 @@
11021119
11031120 /**
11041121 * カラー設定が変更されているか?
1122+ *
11051123 * @return 変更されている場合はtrue、そうでなければfalse
11061124 */
11071125 public boolean isColorConvertParameterModified() {
--- trunk/build.xml (revision 91)
+++ trunk/build.xml (revision 92)
@@ -41,6 +41,8 @@
4141 <include name="**/*.jar"/>
4242 </fileset>
4343 </classpath>
44+ <compilerarg value="-Xlint:deprecation" />
45+ <compilerarg value="-Xlint:unchecked" />
4446 </javac>
4547
4648 <!-- リソースをコピーする -->
@@ -50,6 +52,13 @@
5052 </fileset>
5153 </copy>
5254
55+ <!-- ソース上のリソースをコピーする -->
56+ <copy todir="work">
57+ <fileset dir="src">
58+ <exclude name="**/*.java"/>
59+ </fileset>
60+ </copy>
61+
5362 <!-- JARを作成する -->
5463 <jar jarfile="${distdir}/CharacterManaJ.jar"
5564 basedir="work"
--- trunk/resources/languages/profileselectordialog.xml (revision 91)
+++ trunk/resources/languages/profileselectordialog.xml (revision 92)
@@ -17,7 +17,7 @@
1717 <entry key="description">Description</entry>
1818 <entry key="profiles">Profiles</entry>
1919 <entry key="sample-image">Sample</entry>
20- <entry key="dividerLocation">200</entry>
20+ <entry key="dividerLocation">300</entry>
2121 <entry key="btn.select">Open</entry>
2222 <entry key="btn.cancel">Cancel</entry>
2323 <entry key="dropHere">dropHere</entry>
--- trunk/resources/languages/selectCharatersDirDialog_ja.xml (revision 91)
+++ trunk/resources/languages/selectCharatersDirDialog_ja.xml (revision 92)
@@ -7,5 +7,8 @@
77 <entry key="btn.cancel">キャンセル</entry>
88 <entry key="btn.chooseDir">参照</entry>
99 <entry key="btn.clearRecentList">履歴のクリア</entry>
10+<entry key="btn.clearWorkingSets">キャッシュのクリア</entry>
1011 <entry key="chk.doNotAskAgein">次回から、このフォルダを使用する.</entry>
12+<entry key="confirm.clearWorkingSets">全てのキャッシュをクリアしてもよろしいですか?</entry>
13+<entry key="confirm.clearWorkingSets.title">確認</entry>
1114 </properties>
--- trunk/resources/languages/selectCharatersDirDialog.xml (revision 91)
+++ trunk/resources/languages/selectCharatersDirDialog.xml (revision 92)
@@ -8,6 +8,9 @@
88 <entry key="btn.cancel">Cancel</entry>
99 <entry key="btn.chooseDir">Browse</entry>
1010 <entry key="btn.clearRecentList">Clear recent list</entry>
11+<entry key="btn.clearWorkingSets">Clear cache</entry>
1112 <entry key="chk.doNotAskAgein">Use this as the default and do not ask again.</entry>
13+<entry key="confirm.clearWorkingSets">Are you sure you want to delete all cache?</entry>
14+<entry key="confirm.clearWorkingSets.title">CONFIRM</entry>
1215 </properties>
1316
--- trunk/resources/languages/profileselectordialog_ja.xml (revision 91)
+++ trunk/resources/languages/profileselectordialog_ja.xml (revision 92)
@@ -17,7 +17,7 @@
1717 <entry key="description">プロファイルの説明</entry>
1818 <entry key="profiles">プロファイル一覧</entry>
1919 <entry key="sample-image">サンプルピクチャ</entry>
20- <entry key="dividerLocation">200</entry>
20+ <entry key="dividerLocation">300</entry>
2121 <entry key="btn.select">プロファイルを開く</entry>
2222 <entry key="btn.cancel">キャンセル</entry>
2323 <entry key="dropHere">ここにピクチャをドロップします</entry>
--- trunk/resources/languages/previewpanel_ja.xml (revision 91)
+++ trunk/resources/languages/previewpanel_ja.xml (revision 92)
@@ -5,7 +5,7 @@
55 <entry key="tooltip.copy"><![CDATA[<html>画像をクリップボードにコピーする<br><small>(シフトキーでスクリーンイメージ取得)<small></html>]]></entry>
66 <entry key="tooltip.changeBgColor"><![CDATA[<html>背景を設定する<br><small>(シフトキーで背景色のみ変更)</small></html>]]></entry>
77 <entry key="tooltip.showInformation">情報を表示する</entry>
8- <entry key="tooltip.registerFavorites">お気に入りに追加する</entry>
8+ <entry key="tooltip.registerFavorites"><![CDATA[<html>お気に入りに追加する<br><small>(シフトキーで「お気に入りの管理」)</small></html>]]></entry>
99 <entry key="tooltip.flipHorizontal">画像を左右反転する</entry>
1010
1111 <entry key="tooltip.zoompanel.pinning">固定する</entry>
旧リポジトリブラウザで表示