OSDN Git Service

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