OSDN Git Service

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