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