OSDN Git Service

start 4.101.7-SNAPSHOT
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / Controller.java
1 /*
2  * MVC controller
3  *
4  * License : The MIT License
5  * Copyright(c) 2008 olyutorskii
6  */
7
8 package jp.sfjp.jindolf;
9
10 import java.awt.EventQueue;
11 import java.awt.Frame;
12 import java.awt.Window;
13 import java.awt.event.ActionEvent;
14 import java.awt.event.ActionListener;
15 import java.awt.event.WindowAdapter;
16 import java.awt.event.WindowEvent;
17 import java.io.File;
18 import java.io.IOException;
19 import java.net.URL;
20 import java.text.MessageFormat;
21 import java.util.List;
22 import java.util.logging.Handler;
23 import java.util.logging.Level;
24 import java.util.logging.Logger;
25 import java.util.regex.Pattern;
26 import javax.swing.JButton;
27 import javax.swing.JDialog;
28 import javax.swing.JFileChooser;
29 import javax.swing.JOptionPane;
30 import javax.swing.JToolBar;
31 import javax.swing.JTree;
32 import javax.swing.UnsupportedLookAndFeelException;
33 import javax.swing.WindowConstants;
34 import javax.swing.event.ChangeEvent;
35 import javax.swing.event.ChangeListener;
36 import javax.swing.event.TreeExpansionEvent;
37 import javax.swing.event.TreeSelectionEvent;
38 import javax.swing.event.TreeSelectionListener;
39 import javax.swing.event.TreeWillExpandListener;
40 import javax.swing.filechooser.FileFilter;
41 import javax.swing.filechooser.FileNameExtensionFilter;
42 import javax.swing.tree.TreePath;
43 import jp.sfjp.jindolf.config.AppSetting;
44 import jp.sfjp.jindolf.config.ConfigStore;
45 import jp.sfjp.jindolf.config.JsonIo;
46 import jp.sfjp.jindolf.config.OptionInfo;
47 import jp.sfjp.jindolf.data.Anchor;
48 import jp.sfjp.jindolf.data.DialogPref;
49 import jp.sfjp.jindolf.data.Land;
50 import jp.sfjp.jindolf.data.LandsTreeModel;
51 import jp.sfjp.jindolf.data.Period;
52 import jp.sfjp.jindolf.data.RegexPattern;
53 import jp.sfjp.jindolf.data.Talk;
54 import jp.sfjp.jindolf.data.Village;
55 import jp.sfjp.jindolf.data.html.PeriodLoader;
56 import jp.sfjp.jindolf.data.html.VillageInfoLoader;
57 import jp.sfjp.jindolf.data.html.VillageListLoader;
58 import jp.sfjp.jindolf.data.xml.VillageLoader;
59 import jp.sfjp.jindolf.dxchg.CsvExporter;
60 import jp.sfjp.jindolf.dxchg.WebIPCDialog;
61 import jp.sfjp.jindolf.dxchg.WolfBBS;
62 import jp.sfjp.jindolf.glyph.AnchorHitEvent;
63 import jp.sfjp.jindolf.glyph.AnchorHitListener;
64 import jp.sfjp.jindolf.glyph.Discussion;
65 import jp.sfjp.jindolf.glyph.FontChooser;
66 import jp.sfjp.jindolf.glyph.FontInfo;
67 import jp.sfjp.jindolf.glyph.TalkDraw;
68 import jp.sfjp.jindolf.log.LogFrame;
69 import jp.sfjp.jindolf.log.LogUtils;
70 import jp.sfjp.jindolf.net.ProxyInfo;
71 import jp.sfjp.jindolf.net.ServerAccess;
72 import jp.sfjp.jindolf.summary.DaySummary;
73 import jp.sfjp.jindolf.summary.VillageDigest;
74 import jp.sfjp.jindolf.util.GUIUtils;
75 import jp.sfjp.jindolf.util.StringUtils;
76 import jp.sfjp.jindolf.view.ActionManager;
77 import jp.sfjp.jindolf.view.AvatarPics;
78 import jp.sfjp.jindolf.view.FilterPanel;
79 import jp.sfjp.jindolf.view.FindPanel;
80 import jp.sfjp.jindolf.view.HelpFrame;
81 import jp.sfjp.jindolf.view.LandsTree;
82 import jp.sfjp.jindolf.view.OptionPanel;
83 import jp.sfjp.jindolf.view.PeriodView;
84 import jp.sfjp.jindolf.view.TabBrowser;
85 import jp.sfjp.jindolf.view.TopFrame;
86 import jp.sfjp.jindolf.view.TopView;
87 import jp.sfjp.jindolf.view.WindowManager;
88 import jp.sourceforge.jindolf.corelib.VillageState;
89 import jp.sourceforge.jovsonz.JsObject;
90 import org.xml.sax.SAXException;
91
92 /**
93  * いわゆるMVCでいうとこのコントローラ。
94  */
95 public class Controller
96         implements ActionListener,
97                    AnchorHitListener {
98     private static final Logger LOGGER = Logger.getAnonymousLogger();
99
100     private static final String ERRTITLE_LAF = "Look&Feel";
101     private static final String ERRFORM_LAFGEN =
102             "このLook&Feel[{0}]を生成する事ができません。";
103
104
105     private final LandsTreeModel model;
106     private final WindowManager windowManager;
107     private final ActionManager actionManager;
108     private final AppSetting appSetting;
109
110     private final TopView topView;
111
112     private final JFileChooser xmlFileChooser = buildFileChooser();
113
114     private final VillageTreeWatcher treeVillageWatcher =
115             new VillageTreeWatcher();
116     private final ChangeListener tabPeriodWatcher =
117             new TabPeriodWatcher();
118     private final ChangeListener filterWatcher =
119             new FilterWatcher();
120
121     private final BusyStatus busyStatus;
122
123
124     /**
125      * コントローラの生成。
126      * @param model 最上位データモデル
127      * @param windowManager ウィンドウ管理
128      * @param actionManager アクション管理
129      * @param setting アプリ設定
130      */
131     @SuppressWarnings("LeakingThisInConstructor")
132     public Controller(LandsTreeModel model,
133                       WindowManager windowManager,
134                       ActionManager actionManager,
135                       AppSetting setting){
136         super();
137
138         this.appSetting = setting;
139         this.actionManager = actionManager;
140         this.windowManager = windowManager;
141         this.model = model;
142
143         this.topView = this.windowManager.getTopFrame().getTopView();
144
145         JToolBar toolbar = this.actionManager.getBrowseToolBar();
146         this.topView.setBrowseToolBar(toolbar);
147
148         this.actionManager.addActionListener(this);
149
150         JTree treeView = this.topView.getTreeView();
151         treeView.setModel(this.model);
152         treeView.addTreeWillExpandListener(this.treeVillageWatcher);
153         treeView.addTreeSelectionListener(this.treeVillageWatcher);
154
155         TabBrowser periodTab = this.topView.getTabBrowser();
156         periodTab.addChangeListener(this.tabPeriodWatcher);
157         periodTab.addActionListener(this);
158         periodTab.addAnchorHitListener(this);
159
160         JButton reloadVillageListButton = this.topView
161                                          .getLandsTree()
162                                          .getReloadVillageListButton();
163         reloadVillageListButton.addActionListener(this);
164         reloadVillageListButton.setEnabled(false);
165
166         TopFrame topFrame         = this.windowManager.getTopFrame();
167         OptionPanel optionPanel   = this.windowManager.getOptionPanel();
168         FindPanel findPanel       = this.windowManager.getFindPanel();
169         FilterPanel filterPanel   = this.windowManager.getFilterPanel();
170         LogFrame logFrame         = this.windowManager.getLogFrame();
171         HelpFrame helpFrame       = this.windowManager.getHelpFrame();
172
173         topFrame.setJMenuBar(this.actionManager.getMenuBar());
174         setFrameTitle(null);
175         topFrame.setDefaultCloseOperation(
176                 WindowConstants.DISPOSE_ON_CLOSE);
177         topFrame.addWindowListener(new WindowAdapter(){
178             /** {@inheritDoc} */
179             @Override
180             public void windowClosed(WindowEvent event){
181                 shutdown();
182             }
183         });
184         this.busyStatus = new BusyStatus(topFrame);
185
186         filterPanel.addChangeListener(this.filterWatcher);
187
188         Handler newHandler = logFrame.getHandler();
189         EventQueue.invokeLater(() -> {
190             LogUtils.switchHandler(newHandler);
191         });
192
193         JsonIo jsonIo = this.appSetting.getJsonIo();
194
195         JsObject history = jsonIo.loadHistoryConfig();
196         findPanel.putJson(history);
197
198         FontInfo fontInfo = this.appSetting.getFontInfo();
199         periodTab.setFontInfo(fontInfo);
200         optionPanel.getFontChooser().setFontInfo(fontInfo);
201
202         ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
203         optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
204
205         DialogPref pref = this.appSetting.getDialogPref();
206         periodTab.setDialogPref(pref);
207         optionPanel.getDialogPrefPanel().setDialogPref(pref);
208
209         OptionInfo optInfo = this.appSetting.getOptionInfo();
210         ConfigStore configStore = this.appSetting.getConfigStore();
211         helpFrame.updateVmInfo(optInfo, configStore);
212
213         return;
214     }
215
216
217     /**
218      * フレーム表示のトグル処理。
219      * @param window フレーム
220      */
221     private static void toggleWindow(Window window){
222         if(window == null) return;
223
224         if(window instanceof Frame){
225             Frame frame = (Frame) window;
226             int winState = frame.getExtendedState();
227             boolean isIconified = (winState & Frame.ICONIFIED) != 0;
228             if(isIconified){
229                 winState &= ~(Frame.ICONIFIED);
230                 frame.setExtendedState(winState);
231                 frame.setVisible(true);
232                 return;
233             }
234         }
235
236         if(window.isVisible()){
237             window.setVisible(false);
238             window.dispose();
239         }else{
240             window.setVisible(true);
241         }
242         return;
243     }
244
245     /**
246      * XMLファイルを選択するためのChooserを生成する。
247      *
248      * @return Chooser
249      */
250     private static JFileChooser buildFileChooser(){
251         JFileChooser chooser = new JFileChooser();
252         chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
253
254         FileFilter filter;
255         filter = new FileNameExtensionFilter("XML files (*.xml)", "xml", "XML");
256         chooser.setFileFilter(filter);
257
258         chooser.setDialogTitle("アーカイブXMLファイルを開く");
259
260         return chooser;
261     }
262
263
264     /**
265      * ウィンドウマネジャを返す。
266      * @return ウィンドウマネジャ
267      */
268     public WindowManager getWindowManager(){
269         return this.windowManager;
270     }
271
272     /**
273      * アプリ最上位フレームを返す。
274      * @return アプリ最上位フレーム
275      */
276     public TopFrame getTopFrame(){
277         TopFrame result = this.windowManager.getTopFrame();
278         return result;
279     }
280
281     /**
282      * トップフレームのタイトルを設定する。
283      * タイトルは指定された国or村名 + " - Jindolf"
284      * @param name 国or村名
285      */
286     private void setFrameTitle(String name){
287         String title = VerInfo.getFrameTitle(name);
288         TopFrame topFrame = this.windowManager.getTopFrame();
289         topFrame.setTitle(title);
290         return;
291     }
292
293     /**
294      * 現在選択中のPeriodを内包するPeriodViewを返す。
295      * @return PeriodView
296      */
297     private PeriodView currentPeriodView(){
298         TabBrowser tb = this.topView.getTabBrowser();
299         PeriodView result = tb.currentPeriodView();
300         return result;
301     }
302
303     /**
304      * 現在選択中のPeriodを内包するDiscussionを返す。
305      * @return Discussion
306      */
307     private Discussion currentDiscussion(){
308         PeriodView periodView = currentPeriodView();
309         if(periodView == null) return null;
310         Discussion result = periodView.getDiscussion();
311         return result;
312     }
313
314     /**
315      * 現在選択中の村を返す。
316      *
317      * @return 選択中の村。なければnull。
318      */
319     private Village getVillage(){
320         TabBrowser browser = this.topView.getTabBrowser();
321         Village village = browser.getVillage();
322         return village;
323     }
324
325     /**
326      * ステータスバーを更新する。
327      * @param message メッセージ
328      */
329     private void updateStatusBar(String message){
330         this.topView.updateSysMessage(message);
331         return;
332     }
333
334     /**
335      * About画面を表示する。
336      */
337     private void actionAbout(){
338         String message = VerInfo.getAboutMessage();
339         JOptionPane pane = new JOptionPane(message,
340                                            JOptionPane.INFORMATION_MESSAGE,
341                                            JOptionPane.DEFAULT_OPTION,
342                                            GUIUtils.getLogoIcon());
343
344         JDialog dialog = pane.createDialog(VerInfo.TITLE + "について");
345
346         dialog.pack();
347         dialog.setVisible(true);
348         dialog.dispose();
349
350         return;
351     }
352
353     /**
354      * アプリ終了。
355      */
356     private void actionExit(){
357         shutdown();
358         return;
359     }
360
361     /**
362      * Help画面を表示する。
363      */
364     private void actionHelp(){
365         HelpFrame helpFrame = this.windowManager.getHelpFrame();
366         toggleWindow(helpFrame);
367         return;
368     }
369
370     /**
371      * 村をWebブラウザで表示する。
372      */
373     private void actionShowWebVillage(){
374         Village village = getVillage();
375         if(village == null) return;
376
377         Land land = village.getParentLand();
378         ServerAccess server = land.getServerAccess();
379
380         URL url = server.getVillageURL(village);
381
382         String urlText = url.toString();
383         if(village.getState() != VillageState.GAMEOVER){
384             urlText += "#bottom";
385         }
386
387         WebIPCDialog.showDialog(getTopFrame(), urlText);
388
389         return;
390     }
391
392     /**
393      * 村に対応するまとめサイトをWebブラウザで表示する。
394      */
395     private void actionShowWebWiki(){
396         Village village = getVillage();
397         if(village == null) return;
398
399         String urlTxt = WolfBBS.getCastGeneratorUrl(village);
400         WebIPCDialog.showDialog(getTopFrame(), urlTxt);
401
402         return;
403     }
404
405     /**
406      * 日(Period)をWebブラウザで表示する。
407      */
408     private void actionShowWebDay(){
409         PeriodView periodView = currentPeriodView();
410         if(periodView == null) return;
411
412         Period period = periodView.getPeriod();
413         if(period == null) return;
414
415         Village village = getVillage();
416         if(village == null) return;
417
418         Land land = village.getParentLand();
419         ServerAccess server = land.getServerAccess();
420
421         URL url = server.getPeriodURL(period);
422
423         String urlText = url.toString();
424
425         WebIPCDialog.showDialog(getTopFrame(), urlText);
426
427         return;
428     }
429
430     /**
431      * 個別の発言をWebブラウザで表示する。
432      */
433     private void actionShowWebTalk(){
434         Village village = getVillage();
435         if(village == null) return;
436
437         PeriodView periodView = currentPeriodView();
438         if(periodView == null) return;
439
440         Discussion discussion = periodView.getDiscussion();
441         Talk talk = discussion.getActiveTalk();
442         if(talk == null) return;
443
444         Period period = periodView.getPeriod();
445         if(period == null) return;
446
447         Land land = village.getParentLand();
448         ServerAccess server = land.getServerAccess();
449
450         URL url = server.getPeriodURL(period);
451
452         String urlText = url.toString();
453         urlText += "#" + talk.getMessageID();
454         WebIPCDialog.showDialog(getTopFrame(), urlText);
455
456         return;
457     }
458
459     /**
460      * ポータルサイトをWebブラウザで表示する。
461      */
462     private void actionShowPortal(){
463         WebIPCDialog.showDialog(getTopFrame(), VerInfo.CONTACT);
464         return;
465     }
466
467     /**
468      * 例外発生による警告ダイアログへの反応を促す。
469      * @param title タイトル文字列
470      * @param message メッセージ
471      * @param e 例外
472      */
473     private void warnDialog(String title, String message, Throwable e){
474         LOGGER.log(Level.WARNING, message, e);
475         JOptionPane.showMessageDialog(
476             getTopFrame(),
477             message,
478             VerInfo.getFrameTitle(title),
479             JOptionPane.WARNING_MESSAGE );
480         return;
481     }
482
483     /**
484      * L&Fの変更指示を受信する。
485      */
486     private void actionChangeLaF(){
487         String className = this.actionManager.getSelectedLookAndFeel();
488         if(className == null) return;
489
490         this.busyStatus.submitLightBusyTask(
491             () -> {taskChangeLaF(className);},
492             "Look&Feelを更新中…",
493             "Look&Feelが更新されました"
494         );
495
496         return;
497     }
498
499     /**
500      * LookAndFeelの実際の更新を行う軽量タスク。
501      *
502      * @param lnf LookAndFeel
503      */
504     private void taskChangeLaF(String className){
505         assert EventQueue.isDispatchThread();
506
507         try{
508             this.windowManager.changeAllWindowUI(className);
509         }catch(UnsupportedLookAndFeelException e){
510             String warnMsg = MessageFormat.format(
511                     "このLook&Feel[{0}]はサポートされていません。",
512                     className);
513             warnDialog(ERRTITLE_LAF, warnMsg, e);
514             return;
515         }catch(ReflectiveOperationException e){
516             String warnMsg = MessageFormat.format(ERRFORM_LAFGEN, className);
517             warnDialog(ERRTITLE_LAF, warnMsg, e);
518             return;
519         }
520
521         this.xmlFileChooser.updateUI();
522
523         LOGGER.log(Level.INFO,
524                    "Look&Feelが[{0}]に変更されました。", className );
525
526         return;
527     }
528
529     /**
530      * 発言フィルタ画面を表示する。
531      */
532     private void actionShowFilter(){
533         FilterPanel filterPanel = this.windowManager.getFilterPanel();
534         toggleWindow(filterPanel);
535         return;
536     }
537
538     /**
539      * ログ表示画面を表示する。
540      */
541     private void actionShowLog(){
542         LogFrame logFrame = this.windowManager.getLogFrame();
543         toggleWindow(logFrame);
544         return;
545     }
546
547     /**
548      * オプション設定画面を表示する。
549      */
550     private void actionOption(){
551         OptionPanel optionPanel = this.windowManager.getOptionPanel();
552
553         FontInfo fontInfo = this.appSetting.getFontInfo();
554         optionPanel.getFontChooser().setFontInfo(fontInfo);
555
556         ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
557         optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
558
559         DialogPref dialogPref = this.appSetting.getDialogPref();
560         optionPanel.getDialogPrefPanel().setDialogPref(dialogPref);
561
562         optionPanel.setVisible(true);
563         if(optionPanel.isCanceled()) return;
564
565         fontInfo = optionPanel.getFontChooser().getFontInfo();
566         updateFontInfo(fontInfo);
567
568         proxyInfo = optionPanel.getProxyChooser().getProxyInfo();
569         updateProxyInfo(proxyInfo);
570
571         dialogPref = optionPanel.getDialogPrefPanel().getDialogPref();
572         updateDialogPref(dialogPref);
573
574         return;
575     }
576
577     /**
578      * フォント設定を変更する。
579      * @param newFontInfo 新フォント設定
580      */
581     private void updateFontInfo(final FontInfo newFontInfo){
582         FontInfo oldInfo = this.appSetting.getFontInfo();
583
584         if(newFontInfo.equals(oldInfo)) return;
585         this.appSetting.setFontInfo(newFontInfo);
586
587         this.topView.getTabBrowser().setFontInfo(newFontInfo);
588
589         OptionPanel optionPanel = this.windowManager.getOptionPanel();
590         FontChooser fontChooser = optionPanel.getFontChooser();
591
592         fontChooser.setFontInfo(newFontInfo);
593
594         return;
595     }
596
597     /**
598      * プロクシ設定を変更する。
599      * @param newProxyInfo 新プロクシ設定
600      */
601     private void updateProxyInfo(ProxyInfo newProxyInfo){
602         ProxyInfo oldProxyInfo = this.appSetting.getProxyInfo();
603
604         if(newProxyInfo.equals(oldProxyInfo)) return;
605         this.appSetting.setProxyInfo(newProxyInfo);
606
607         for(Land land : this.model.getLandList()){
608             ServerAccess server = land.getServerAccess();
609             server.setProxy(newProxyInfo.getProxy());
610         }
611
612         return;
613     }
614
615     /**
616      * 発言表示設定を更新する。
617      * @param newDialogPref 表示設定
618      */
619     private void updateDialogPref(DialogPref newDialogPref){
620         DialogPref oldDialogPref = this.appSetting.getDialogPref();
621
622         if(newDialogPref.equals(oldDialogPref)) return;
623         this.appSetting.setDialogPref(newDialogPref);
624
625         this.topView.getTabBrowser().setDialogPref(newDialogPref);
626
627         return;
628     }
629
630     /**
631      * 村ダイジェスト画面を表示する。
632      */
633     private void actionShowDigest(){
634         Village village = getVillage();
635         if(village == null) return;
636
637         VillageState villageState = village.getState();
638         if( (   villageState != VillageState.EPILOGUE
639              && villageState != VillageState.GAMEOVER
640             ) || ! village.isValid() ){
641             String message = "エピローグを正常に迎えていない村は\n"
642                             +"ダイジェスト機能を利用できません";
643             String title = VerInfo.getFrameTitle("ダイジェスト不可");
644             JOptionPane pane = new JOptionPane(message,
645                                                JOptionPane.WARNING_MESSAGE,
646                                                JOptionPane.DEFAULT_OPTION );
647             JDialog dialog = pane.createDialog(title);
648             dialog.pack();
649             dialog.setVisible(true);
650             dialog.dispose();
651             return;
652         }
653
654         VillageDigest villageDigest = this.windowManager.getVillageDigest();
655         final VillageDigest digest = villageDigest;
656
657         Runnable task = () -> {
658             taskFullOpenAllPeriod();
659             EventQueue.invokeLater(() -> {
660                 digest.setVillage(village);
661                 digest.setVisible(true);
662             });
663         };
664
665         this.busyStatus.submitHeavyBusyTask(
666                 task,
667                 "一括読み込み開始",
668                 "一括読み込み完了"
669         );
670
671         return;
672     }
673
674     /**
675      * 全日程の一括フルオープン。ヘビータスク版。
676      */
677     // TODO taskLoadAllPeriodtと一体化したい。
678     private void taskFullOpenAllPeriod(){
679         TabBrowser browser = this.topView.getTabBrowser();
680         Village village = getVillage();
681         if(village == null) return;
682         for(PeriodView periodView : browser.getPeriodViewList()){
683             Period period = periodView.getPeriod();
684             if(period == null) continue;
685             String message =
686                     period.getDay()
687                     + "日目のデータを読み込んでいます";
688             updateStatusBar(message);
689             try{
690                 PeriodLoader.parsePeriod(period, false);
691             }catch(IOException e){
692                 showNetworkError(village, e);
693                 return;
694             }
695             periodView.showTopics();
696         }
697
698         return;
699     }
700
701     /**
702      * 検索パネルを表示する。
703      */
704     private void actionShowFind(){
705         FindPanel findPanel = this.windowManager.getFindPanel();
706
707         findPanel.setVisible(true);
708         if(findPanel.isCanceled()){
709             updateFindPanel();
710             return;
711         }
712         if(findPanel.isBulkSearch()){
713             bulkSearch();
714         }else{
715             regexSearch();
716         }
717         return;
718     }
719
720     /**
721      * 検索処理。
722      */
723     private void regexSearch(){
724         Discussion discussion = currentDiscussion();
725         if(discussion == null) return;
726
727         FindPanel findPanel = this.windowManager.getFindPanel();
728         RegexPattern regPattern = findPanel.getRegexPattern();
729         int hits = discussion.setRegexPattern(regPattern);
730
731         String hitMessage = "[" + hits + "]件ヒットしました";
732         updateStatusBar(hitMessage);
733
734         String loginfo = "";
735         if(regPattern != null){
736             Pattern pattern = regPattern.getPattern();
737             if(pattern != null){
738                 loginfo = "正規表現 " + pattern.pattern() + " に";
739             }
740         }
741         loginfo += hitMessage;
742         LOGGER.info(loginfo);
743
744         return;
745     }
746
747     /**
748      * 一括検索処理。
749      */
750     private void bulkSearch(){
751         this.busyStatus.submitHeavyBusyTask(
752                 () -> {taskBulkSearch();},
753                 null, null
754         );
755         return;
756     }
757
758     /**
759      * 一括検索処理。ヘビータスク版。
760      */
761     private void taskBulkSearch(){
762         taskLoadAllPeriod();
763         int totalhits = 0;
764         FindPanel findPanel = this.windowManager.getFindPanel();
765         RegexPattern regPattern = findPanel.getRegexPattern();
766         StringBuilder hitDesc = new StringBuilder();
767         TabBrowser browser = this.topView.getTabBrowser();
768         for(PeriodView periodView : browser.getPeriodViewList()){
769             Discussion discussion = periodView.getDiscussion();
770             int hits = discussion.setRegexPattern(regPattern);
771             totalhits += hits;
772
773             if(hits > 0){
774                 Period period = discussion.getPeriod();
775                 hitDesc.append(' ').append(period.getDay()).append("d:");
776                 hitDesc.append(hits).append("件");
777             }
778         }
779         String hitMessage =
780                   "[" + totalhits + "]件ヒットしました。"
781                 + hitDesc.toString();
782         updateStatusBar(hitMessage);
783
784         String loginfo = "";
785         if(regPattern != null){
786             Pattern pattern = regPattern.getPattern();
787             if(pattern != null){
788                 loginfo = "正規表現 " + pattern.pattern() + " に";
789             }
790         }
791         loginfo += hitMessage;
792         LOGGER.info(loginfo);
793
794         return;
795     }
796
797     /**
798      * 検索パネルに現在選択中のPeriodを反映させる。
799      */
800     private void updateFindPanel(){
801         Discussion discussion = currentDiscussion();
802         if(discussion == null) return;
803         RegexPattern pattern = discussion.getRegexPattern();
804         FindPanel findPanel = this.windowManager.getFindPanel();
805         findPanel.setRegexPattern(pattern);
806         return;
807     }
808
809     /**
810      * 発言集計パネルを表示。
811      */
812     private void actionDaySummary(){
813         PeriodView periodView = currentPeriodView();
814         if(periodView == null) return;
815
816         Period period = periodView.getPeriod();
817         if(period == null) return;
818
819         DaySummary daySummary = this.windowManager.getDaySummary();
820         daySummary.summaryPeriod(period);
821         daySummary.setVisible(true);
822
823         return;
824     }
825
826     /**
827      * 表示中PeriodをCSVファイルへエクスポートする。
828      */
829     private void actionDayExportCsv(){
830         PeriodView periodView = currentPeriodView();
831         if(periodView == null) return;
832
833         Period period = periodView.getPeriod();
834         if(period == null) return;
835
836         FilterPanel filterPanel = this.windowManager.getFilterPanel();
837         File file = CsvExporter.exportPeriod(period, filterPanel);
838         if(file != null){
839             String message = "CSVファイル("
840                             +file.getName()
841                             +")へのエクスポートが完了しました";
842             updateStatusBar(message);
843         }
844
845         // TODO 長そうなジョブなら別スレッドにした方がいいか?
846
847         return;
848     }
849
850     /**
851      * 検索結果の次候補へジャンプ。
852      */
853     private void actionSearchNext(){
854         Discussion discussion = currentDiscussion();
855         if(discussion == null) return;
856
857         discussion.nextHotTarget();
858
859         return;
860     }
861
862     /**
863      * 検索結果の全候補へジャンプ。
864      */
865     private void actionSearchPrev(){
866         Discussion discussion = currentDiscussion();
867         if(discussion == null) return;
868
869         discussion.prevHotTarget();
870
871         return;
872     }
873
874     /**
875      * Period表示の強制再更新処理。
876      */
877     private void actionReloadPeriod(){
878         updatePeriod(true);
879
880         Village village = getVillage();
881         if(village == null) return;
882         if(village.getState() != VillageState.EPILOGUE) return;
883
884         Discussion discussion = currentDiscussion();
885         if(discussion == null) return;
886         Period period = discussion.getPeriod();
887         if(period == null) return;
888         if(period.getTopics() > 1000){
889             JOptionPane.showMessageDialog(getTopFrame(),
890                     "エピローグが1000発言を超えはじめたら、\n"
891                     +"負荷対策のためWebブラウザによるアクセスを"
892                     +"心がけましょう",
893                     "長大エピローグ警告",
894                     JOptionPane.WARNING_MESSAGE
895             );
896         }
897
898         return;
899     }
900
901     /**
902      * 全日程の一括ロード。
903      */
904     private void actionLoadAllPeriod(){
905         this.busyStatus.submitHeavyBusyTask(
906                 () -> {taskLoadAllPeriod();},
907                 "一括読み込み開始",
908                 "一括読み込み完了"
909         );
910
911         return;
912     }
913
914     /**
915      * 全日程の一括ロード。ヘビータスク版。
916      */
917     private void taskLoadAllPeriod(){
918         TabBrowser browser = this.topView.getTabBrowser();
919         Village village = getVillage();
920         if(village == null) return;
921         for(PeriodView periodView : browser.getPeriodViewList()){
922             Period period = periodView.getPeriod();
923             if(period == null) continue;
924             String message =
925                     period.getDay()
926                     + "日目のデータを読み込んでいます";
927             updateStatusBar(message);
928             try{
929                 PeriodLoader.parsePeriod(period, false);
930             }catch(IOException e){
931                 showNetworkError(village, e);
932                 return;
933             }
934             periodView.showTopics();
935         }
936
937         return;
938     }
939
940     /**
941      * 村一覧の再読み込み。
942      */
943     private void actionReloadVillageList(){
944         JTree tree = this.topView.getTreeView();
945         TreePath path = tree.getSelectionPath();
946         if(path == null) return;
947
948         Land land = null;
949         for(int ct = 0; ct < path.getPathCount(); ct++){
950             Object obj = path.getPathComponent(ct);
951             if(obj instanceof Land){
952                 land = (Land) obj;
953                 break;
954             }
955         }
956         if(land == null) return;
957
958         this.topView.showInitPanel();
959
960         submitReloadVillageList(land);
961
962         return;
963     }
964
965     /**
966      * 選択文字列をクリップボードにコピーする。
967      */
968     private void actionCopySelected(){
969         Discussion discussion = currentDiscussion();
970         if(discussion == null) return;
971
972         CharSequence copied = discussion.copySelected();
973         if(copied == null) return;
974
975         copied = StringUtils.suppressString(copied);
976         updateStatusBar(
977                 "[" + copied + "]をクリップボードにコピーしました");
978         return;
979     }
980
981     /**
982      * 一発言のみクリップボードにコピーする。
983      */
984     private void actionCopyTalk(){
985         Discussion discussion = currentDiscussion();
986         if(discussion == null) return;
987
988         CharSequence copied = discussion.copyTalk();
989         if(copied == null) return;
990
991         copied = StringUtils.suppressString(copied);
992         updateStatusBar(
993                 "[" + copied + "]をクリップボードにコピーしました");
994         return;
995     }
996
997     /**
998      * アンカー先を含むPeriodの全会話を事前にロードする。
999      *
1000      * @param village 村
1001      * @param anchor アンカー
1002      * @return アンカー先を含むPeriod。
1003      * アンカーがG国発言番号ならnull。
1004      * Periodが見つからないならnull。
1005      * @throws IOException 入力エラー
1006      */
1007     private Period loadAnchoredPeriod(Village village, Anchor anchor)
1008             throws IOException{
1009         if(anchor.hasTalkNo()) return null;
1010
1011         Period anchorPeriod = village.getPeriod(anchor);
1012         if(anchorPeriod == null) return null;
1013
1014         PeriodLoader.parsePeriod(anchorPeriod, false);
1015
1016         return anchorPeriod;
1017     }
1018
1019     /**
1020      * アンカーにジャンプする。
1021      */
1022     private void actionJumpAnchor(){
1023         PeriodView periodView = currentPeriodView();
1024         if(periodView == null) return;
1025         Discussion discussion = periodView.getDiscussion();
1026
1027         TabBrowser browser = this.topView.getTabBrowser();
1028         Village village = getVillage();
1029         final Anchor anchor = discussion.getActiveAnchor();
1030         if(anchor == null) return;
1031
1032         Runnable task = () -> {
1033             if(anchor.hasTalkNo()){
1034                 // TODO もう少し賢くならない?
1035                 taskLoadAllPeriod();
1036             }
1037
1038             final List<Talk> talkList;
1039             try{
1040                 loadAnchoredPeriod(village, anchor);
1041                 talkList = village.getTalkListFromAnchor(anchor);
1042                 if(talkList == null || talkList.size() <= 0){
1043                     updateStatusBar(
1044                             "アンカーのジャンプ先["
1045                                     + anchor.toString()
1046                                     + "]が見つかりません");
1047                     return;
1048                 }
1049
1050                 Talk targetTalk = talkList.get(0);
1051                 Period targetPeriod = targetTalk.getPeriod();
1052                 int periodIndex = targetPeriod.getDay();
1053                 PeriodView target = browser.getPeriodView(periodIndex);
1054
1055                 EventQueue.invokeLater(() -> {
1056                     browser.showPeriodTab(periodIndex);
1057                     target.setPeriod(targetPeriod);
1058                     target.scrollToTalk(targetTalk);
1059                 });
1060                 updateStatusBar(
1061                         "アンカー["
1062                                 + anchor.toString()
1063                                 + "]にジャンプしました");
1064             }catch(IOException e){
1065                 updateStatusBar(
1066                         "アンカーの展開中にエラーが起きました");
1067             }
1068
1069         };
1070
1071         this.busyStatus.submitHeavyBusyTask(
1072                 task,
1073                 "ジャンプ先の読み込み中…",
1074                 null
1075         );
1076
1077         return;
1078     }
1079
1080     /**
1081      * ローカルなXMLファイルを読み込む。
1082      */
1083     private void actionOpenXml(){
1084         int result = this.xmlFileChooser.showOpenDialog(getTopFrame());
1085         if(result != JFileChooser.APPROVE_OPTION) return;
1086         File selected = this.xmlFileChooser.getSelectedFile();
1087
1088         this.busyStatus.submitHeavyBusyTask(() -> {
1089             Village village;
1090
1091             try{
1092                 village = VillageLoader.parseVillage(selected);
1093             }catch(IOException e){
1094                 String warnMsg = MessageFormat.format(
1095                         "XMLファイル[ {0} ]を読み込むことができません",
1096                         selected.getPath()
1097                 );
1098                 warnDialog("XML I/O error", warnMsg, e);
1099                 return;
1100             }catch(SAXException e){
1101                 String warnMsg = MessageFormat.format(
1102                         "XMLファイル[ {0} ]の形式が不正なため読み込むことができません",
1103                         selected.getPath()
1104                 );
1105                 warnDialog("XML form error", warnMsg, e);
1106                 return;
1107             }
1108
1109             village.setLocalArchive(true);
1110             AvatarPics avatarPics = village.getAvatarPics();
1111             this.appSetting.applyLocalImage(avatarPics);
1112             avatarPics.preload();
1113             EventQueue.invokeLater(() -> {
1114                 selectedVillage(village);
1115             });
1116         }, "XML読み込み中", "XML読み込み完了");
1117
1118         return;
1119     }
1120
1121     /**
1122      * 指定した国の村一覧を読み込むジョブを投下。
1123      * @param land 国
1124      */
1125     private void submitReloadVillageList(final Land land){
1126         this.busyStatus.submitHeavyBusyTask(
1127             () -> {taskReloadVillageList(land);},
1128             "村一覧を読み込み中…",
1129             "村一覧の読み込み完了"
1130         );
1131         return;
1132     }
1133
1134     /**
1135      * 指定した国の村一覧を読み込む。(ヘビータスク本体).
1136      * @param land 国
1137      */
1138     private void taskReloadVillageList(Land land){
1139         List<Village> villageList;
1140         try{
1141             villageList = VillageListLoader.loadVillageList(land);
1142         }catch(IOException e){
1143             showNetworkError(land, e);
1144             return;
1145         }
1146         land.updateVillageList(villageList);
1147
1148         this.model.updateVillageList(land);
1149
1150         LandsTree treePanel = this.topView.getLandsTree();
1151         treePanel.expandLand(land);
1152
1153         return;
1154     }
1155
1156     /**
1157      * Period表示の更新処理。
1158      * @param force trueならPeriodデータを強制再読み込み。
1159      */
1160     private void updatePeriod(final boolean force){
1161         Village village = getVillage();
1162         if(village == null) return;
1163
1164         String fullName = village.getVillageFullName();
1165         setFrameTitle(fullName);
1166
1167         PeriodView periodView = currentPeriodView();
1168         Discussion discussion = currentDiscussion();
1169         if(discussion == null) return;
1170
1171         FilterPanel filterPanel = this.windowManager.getFilterPanel();
1172         discussion.setTopicFilter(filterPanel);
1173
1174         Period period = discussion.getPeriod();
1175         if(period == null) return;
1176
1177         Runnable task = () -> {
1178             try{
1179                 PeriodLoader.parsePeriod(period, force);
1180             }catch(IOException e){
1181                 showNetworkError(village, e);
1182                 return;
1183             }
1184
1185             EventQueue.invokeLater(() -> {
1186                 int lastPos = periodView.getVerticalPosition();
1187                 periodView.showTopics();
1188                 periodView.setVerticalPosition(lastPos);
1189             });
1190         };
1191
1192         this.busyStatus.submitHeavyBusyTask(
1193                 task,
1194                 "会話の読み込み中",
1195                 "会話の表示が完了"
1196         );
1197
1198         return;
1199     }
1200
1201     /**
1202      * 発言フィルタの操作による更新処理。
1203      */
1204     private void filterChanged(){
1205         final Discussion discussion = currentDiscussion();
1206         if(discussion == null) return;
1207
1208         FilterPanel filterPanel = this.windowManager.getFilterPanel();
1209
1210         discussion.setTopicFilter(filterPanel);
1211         discussion.filtering();
1212
1213         return;
1214     }
1215
1216     /**
1217      * ネットワークエラーを通知するモーダルダイアログを表示する。
1218      * OKボタンを押すまでこのメソッドは戻ってこない。
1219      * @param village 村
1220      * @param e ネットワークエラー
1221      */
1222     public void showNetworkError(Village village, IOException e){
1223         Land land = village.getParentLand();
1224         showNetworkError(land, e);
1225         return;
1226     }
1227
1228     /**
1229      * ネットワークエラーを通知するモーダルダイアログを表示する。
1230      * OKボタンを押すまでこのメソッドは戻ってこない。
1231      * @param land 国
1232      * @param e ネットワークエラー
1233      */
1234     public void showNetworkError(Land land, IOException e){
1235         LOGGER.log(Level.WARNING, "ネットワークで障害が発生しました", e);
1236
1237         ServerAccess server = land.getServerAccess();
1238         String message =
1239                 land.getLandDef().getLandName()
1240                 +"を運営するサーバとの間の通信で"
1241                 +"何らかのトラブルが発生しました。\n"
1242                 +"相手サーバのURLは [ " + server.getBaseURL() + " ] だよ。\n"
1243                 +"プロクシ設定は正しいかな?\n"
1244                 +"Webブラウザでも遊べないか確認してみてね!\n";
1245
1246         JOptionPane pane = new JOptionPane(message,
1247                                            JOptionPane.WARNING_MESSAGE,
1248                                            JOptionPane.DEFAULT_OPTION );
1249
1250         String title = VerInfo.getFrameTitle("通信異常発生");
1251         JDialog dialog = pane.createDialog(title);
1252
1253         dialog.pack();
1254         dialog.setVisible(true);
1255         dialog.dispose();
1256
1257         return;
1258     }
1259
1260     /**
1261      * 国を選択する。
1262      *
1263      * @param land 国
1264      */
1265     private void selectedLand(Land land){
1266         String landName = land.getLandDef().getLandName();
1267         setFrameTitle(landName);
1268
1269         this.actionManager.exposeVillage(false);
1270         this.actionManager.exposePeriod(false);
1271
1272         this.topView.showLandInfo(land);
1273
1274         return;
1275     }
1276
1277     /**
1278      * 村を選択する。
1279      *
1280      * @param village 村
1281      */
1282     private void selectedVillage(Village village){
1283         setFrameTitle(village.getVillageFullName());
1284         if(village.isLocalArchive()){
1285             this.actionManager.exposeVillageLocal(true);
1286         }else{
1287             this.actionManager.exposeVillage(true);
1288         }
1289
1290         Runnable task = () -> {
1291             try{
1292                 if( ! village.hasSchedule() ){
1293                     VillageInfoLoader.updateVillageInfo(village);
1294                 }
1295             }catch(IOException e){
1296                 showNetworkError(village, e);
1297                 return;
1298             }
1299
1300             EventQueue.invokeLater(() -> {
1301                 this.topView.showVillageInfo(village);
1302             });
1303         };
1304
1305         this.busyStatus.submitHeavyBusyTask(
1306                 task,
1307                 "村情報を読み込み中…",
1308                 "村情報の読み込み完了"
1309         );
1310
1311         return;
1312     }
1313
1314     /**
1315      * {@inheritDoc}
1316      *
1317      * <p>主にメニュー選択やボタン押下などのアクションをディスパッチする。
1318      *
1319      * <p>ビジーな状態では何もしない。
1320      *
1321      * @param ev {@inheritDoc}
1322      */
1323     @Override
1324     public void actionPerformed(ActionEvent ev){
1325         if(this.busyStatus.isBusy()) return;
1326
1327         String cmd = ev.getActionCommand();
1328         if(cmd == null) return;
1329
1330         switch(cmd){
1331         case ActionManager.CMD_OPENXML:
1332             actionOpenXml();
1333             break;
1334         case ActionManager.CMD_EXIT:
1335             actionExit();
1336             break;
1337         case ActionManager.CMD_COPY:
1338             actionCopySelected();
1339             break;
1340         case ActionManager.CMD_SHOWFIND:
1341             actionShowFind();
1342             break;
1343         case ActionManager.CMD_SEARCHNEXT:
1344             actionSearchNext();
1345             break;
1346         case ActionManager.CMD_SEARCHPREV:
1347             actionSearchPrev();
1348             break;
1349         case ActionManager.CMD_ALLPERIOD:
1350             actionLoadAllPeriod();
1351             break;
1352         case ActionManager.CMD_SHOWDIGEST:
1353             actionShowDigest();
1354             break;
1355         case ActionManager.CMD_WEBVILL:
1356             actionShowWebVillage();
1357             break;
1358         case ActionManager.CMD_WEBWIKI:
1359             actionShowWebWiki();
1360             break;
1361         case ActionManager.CMD_RELOAD:
1362             actionReloadPeriod();
1363             break;
1364         case ActionManager.CMD_DAYSUMMARY:
1365             actionDaySummary();
1366             break;
1367         case ActionManager.CMD_DAYEXPCSV:
1368             actionDayExportCsv();
1369             break;
1370         case ActionManager.CMD_WEBDAY:
1371             actionShowWebDay();
1372             break;
1373         case ActionManager.CMD_OPTION:
1374             actionOption();
1375             break;
1376         case ActionManager.CMD_LANDF:
1377             actionChangeLaF();
1378             break;
1379         case ActionManager.CMD_SHOWFILT:
1380             actionShowFilter();
1381             break;
1382         case ActionManager.CMD_SHOWLOG:
1383             actionShowLog();
1384             break;
1385         case ActionManager.CMD_HELPDOC:
1386             actionHelp();
1387             break;
1388         case ActionManager.CMD_SHOWPORTAL:
1389             actionShowPortal();
1390             break;
1391         case ActionManager.CMD_ABOUT:
1392             actionAbout();
1393             break;
1394         case ActionManager.CMD_VILLAGELIST:
1395             actionReloadVillageList();
1396             break;
1397         case ActionManager.CMD_COPYTALK:
1398             actionCopyTalk();
1399             break;
1400         case ActionManager.CMD_JUMPANCHOR:
1401             actionJumpAnchor();
1402             break;
1403         case ActionManager.CMD_WEBTALK:
1404             actionShowWebTalk();
1405             break;
1406         default:
1407             break;
1408         }
1409
1410         return;
1411     }
1412
1413     /**
1414      * {@inheritDoc}
1415      * @param event {@inheritDoc}
1416      */
1417     @Override
1418     public void anchorHitted(AnchorHitEvent event){
1419         PeriodView periodView = currentPeriodView();
1420         if(periodView == null) return;
1421         Period period = periodView.getPeriod();
1422         if(period == null) return;
1423         final Village village = period.getVillage();
1424
1425         final TalkDraw talkDraw = event.getTalkDraw();
1426         final Anchor anchor = event.getAnchor();
1427         final Discussion discussion = periodView.getDiscussion();
1428
1429         Runnable task = () -> {
1430             if(anchor.hasTalkNo()){
1431                 // TODO もう少し賢くならない?
1432                 taskLoadAllPeriod();
1433             }
1434
1435             final List<Talk> talkList;
1436             try{
1437                 loadAnchoredPeriod(village, anchor);
1438                 talkList = village.getTalkListFromAnchor(anchor);
1439                 if(talkList == null || talkList.size() <= 0){
1440                     updateStatusBar(
1441                             "アンカーの展開先["
1442                                     + anchor.toString()
1443                                     + "]が見つかりません");
1444                     return;
1445                 }
1446                 EventQueue.invokeLater(() -> {
1447                     talkDraw.showAnchorTalks(anchor, talkList);
1448                     discussion.layoutRows();
1449                 });
1450                 updateStatusBar(
1451                         "アンカー["
1452                                 + anchor.toString()
1453                                 + "]の展開完了");
1454             }catch(IOException e){
1455                 updateStatusBar(
1456                         "アンカーの展開中にエラーが起きました");
1457             }
1458         };
1459
1460         this.busyStatus.submitHeavyBusyTask(
1461                 task,
1462                 "アンカーの展開中…",
1463                 null
1464         );
1465
1466         return;
1467     }
1468
1469     /**
1470      * アプリ正常終了処理。
1471      */
1472     private void shutdown(){
1473         JsonIo jsonIo = this.appSetting.getJsonIo();
1474
1475         FindPanel findPanel = this.windowManager.getFindPanel();
1476         JsObject findConf = findPanel.getJson();
1477         if( ! findPanel.hasConfChanged(findConf) ){
1478             jsonIo.saveHistoryConfig(findConf);
1479         }
1480
1481         this.appSetting.saveConfig();
1482
1483         LOGGER.info("VMごとアプリケーションを終了します。");
1484         System.exit(0);  // invoke shutdown hooks... BYE !
1485
1486         assert false;
1487         return;
1488     }
1489
1490
1491     /**
1492      * 発言フィルタ操作を監視する。
1493      */
1494     private class FilterWatcher implements ChangeListener{
1495
1496         /**
1497          * constructor.
1498          */
1499         FilterWatcher(){
1500             super();
1501             return;
1502         }
1503
1504
1505         /**
1506          * {@inheritDoc}
1507          *
1508          * <p>発言フィルタが操作されたときの処理。
1509          *
1510          * @param event {@inheritDoc}
1511          */
1512         @Override
1513         public void stateChanged(ChangeEvent event){
1514             Object source = event.getSource();
1515
1516             if(source == Controller.this.windowManager.getFilterPanel()){
1517                 filterChanged();
1518             }
1519
1520             return;
1521         }
1522
1523     }
1524
1525     /**
1526      * Period一覧タブのタブ操作を監視する。
1527      */
1528     private class TabPeriodWatcher implements ChangeListener{
1529
1530         /**
1531          * constructor.
1532          */
1533         TabPeriodWatcher(){
1534             super();
1535             return;
1536         }
1537
1538
1539         /**
1540          * {@inheritDoc}
1541          *
1542          * <p>Periodがタブ選択されたときの処理。
1543          *
1544          * @param event {@inheritDoc}
1545          */
1546         @Override
1547         public void stateChanged(ChangeEvent event){
1548             Object source = event.getSource();
1549
1550             if(source instanceof TabBrowser){
1551                 updateFindPanel();
1552                 updatePeriod(false);
1553                 PeriodView periodView = currentPeriodView();
1554                 boolean hasCurrentPeriod;
1555                 if(periodView == null) hasCurrentPeriod = false;
1556                 else                   hasCurrentPeriod = true;
1557                 Controller.this.actionManager.exposePeriod(hasCurrentPeriod);
1558                 if(hasCurrentPeriod){
1559                     Village village = getVillage();
1560                     if(village.isLocalArchive()){
1561                         Controller.this.actionManager.exposeVillageLocal(hasCurrentPeriod);
1562                     }else{
1563                         Controller.this.actionManager.exposeVillage(hasCurrentPeriod);
1564                     }
1565                 }
1566             }
1567
1568             return;
1569         }
1570
1571     }
1572
1573     /**
1574      * 国村選択リストの選択展開操作を監視する。
1575      */
1576     private class VillageTreeWatcher
1577             implements TreeSelectionListener, TreeWillExpandListener{
1578
1579         /**
1580          * Constructor.
1581          */
1582         VillageTreeWatcher(){
1583             super();
1584             return;
1585         }
1586
1587
1588         /**
1589          * {@inheritDoc}
1590          *
1591          * <p>ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
1592          *
1593          * @param event {@inheritDoc}
1594          */
1595         @Override
1596         public void valueChanged(TreeSelectionEvent event){
1597             TreePath path = event.getNewLeadSelectionPath();
1598             if(path == null) return;
1599
1600             Object selObj = path.getLastPathComponent();
1601             if(selObj instanceof Land){
1602                 Land land = (Land) selObj;
1603                 selectedLand(land);
1604             }else if(selObj instanceof Village){
1605                 Village village = (Village) selObj;
1606                 village.setLocalArchive(false);
1607                 selectedVillage(village);
1608             }
1609
1610             return;
1611         }
1612
1613         /**
1614          * {@inheritDoc}
1615          *
1616          * <p>村選択ツリーリストが畳まれるとき呼ばれる。
1617          *
1618          * @param event ツリーイベント {@inheritDoc}
1619          */
1620         @Override
1621         public void treeWillCollapse(TreeExpansionEvent event){
1622             return;
1623         }
1624
1625         /**
1626          * {@inheritDoc}
1627          *
1628          * <p>村選択ツリーリストが展開されるとき呼ばれる。
1629          *
1630          * @param event ツリーイベント {@inheritDoc}
1631          */
1632         @Override
1633         public void treeWillExpand(TreeExpansionEvent event){
1634             if(!(event.getSource() instanceof JTree)){
1635                 return;
1636             }
1637
1638             TreePath path = event.getPath();
1639             Object lastObj = path.getLastPathComponent();
1640             if(!(lastObj instanceof Land)){
1641                 return;
1642             }
1643             Land land = (Land) lastObj;
1644             if(land.getVillageCount() > 0){
1645                 return;
1646             }
1647
1648             submitReloadVillageList(land);
1649
1650             return;
1651         }
1652
1653     }
1654
1655 }