OSDN Git Service

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