4 * License : The MIT License
5 * Copyright(c) 2008 olyutorskii
8 package jp.sfjp.jindolf;
10 import java.awt.Component;
11 import java.awt.Cursor;
12 import java.awt.EventQueue;
13 import java.awt.Frame;
14 import java.awt.Window;
15 import java.awt.event.ActionEvent;
16 import java.awt.event.ActionListener;
17 import java.awt.event.WindowAdapter;
18 import java.awt.event.WindowEvent;
20 import java.io.IOException;
21 import java.io.UnsupportedEncodingException;
22 import java.lang.reflect.InvocationTargetException;
24 import java.net.URLEncoder;
25 import java.text.MessageFormat;
26 import java.util.List;
27 import java.util.SortedSet;
28 import java.util.concurrent.Executor;
29 import java.util.concurrent.Executors;
30 import java.util.logging.Handler;
31 import java.util.logging.Level;
32 import java.util.logging.Logger;
33 import java.util.regex.Pattern;
34 import javax.swing.JButton;
35 import javax.swing.JDialog;
36 import javax.swing.JOptionPane;
37 import javax.swing.JToolBar;
38 import javax.swing.JTree;
39 import javax.swing.LookAndFeel;
40 import javax.swing.SwingUtilities;
41 import javax.swing.UIManager;
42 import javax.swing.UnsupportedLookAndFeelException;
43 import javax.swing.WindowConstants;
44 import javax.swing.event.ChangeEvent;
45 import javax.swing.event.ChangeListener;
46 import javax.swing.event.TreeExpansionEvent;
47 import javax.swing.event.TreeSelectionEvent;
48 import javax.swing.event.TreeSelectionListener;
49 import javax.swing.event.TreeWillExpandListener;
50 import javax.swing.tree.TreePath;
51 import jp.sfjp.jindolf.config.AppSetting;
52 import jp.sfjp.jindolf.config.ConfigStore;
53 import jp.sfjp.jindolf.config.OptionInfo;
54 import jp.sfjp.jindolf.data.Anchor;
55 import jp.sfjp.jindolf.data.DialogPref;
56 import jp.sfjp.jindolf.data.Land;
57 import jp.sfjp.jindolf.data.LandsModel;
58 import jp.sfjp.jindolf.data.Period;
59 import jp.sfjp.jindolf.data.RegexPattern;
60 import jp.sfjp.jindolf.data.Talk;
61 import jp.sfjp.jindolf.data.Village;
62 import jp.sfjp.jindolf.dxchg.CsvExporter;
63 import jp.sfjp.jindolf.dxchg.WebIPCDialog;
64 import jp.sfjp.jindolf.editor.TalkPreview;
65 import jp.sfjp.jindolf.glyph.AnchorHitEvent;
66 import jp.sfjp.jindolf.glyph.AnchorHitListener;
67 import jp.sfjp.jindolf.glyph.Discussion;
68 import jp.sfjp.jindolf.glyph.FontChooser;
69 import jp.sfjp.jindolf.glyph.FontInfo;
70 import jp.sfjp.jindolf.glyph.TalkDraw;
71 import jp.sfjp.jindolf.log.LogFrame;
72 import jp.sfjp.jindolf.log.LogUtils;
73 import jp.sfjp.jindolf.net.ProxyInfo;
74 import jp.sfjp.jindolf.net.ServerAccess;
75 import jp.sfjp.jindolf.summary.DaySummary;
76 import jp.sfjp.jindolf.summary.VillageDigest;
77 import jp.sfjp.jindolf.util.GUIUtils;
78 import jp.sfjp.jindolf.util.StringUtils;
79 import jp.sfjp.jindolf.view.AccountPanel;
80 import jp.sfjp.jindolf.view.ActionManager;
81 import jp.sfjp.jindolf.view.FilterPanel;
82 import jp.sfjp.jindolf.view.FindPanel;
83 import jp.sfjp.jindolf.view.HelpFrame;
84 import jp.sfjp.jindolf.view.LandsTree;
85 import jp.sfjp.jindolf.view.OptionPanel;
86 import jp.sfjp.jindolf.view.PeriodView;
87 import jp.sfjp.jindolf.view.TabBrowser;
88 import jp.sfjp.jindolf.view.TopFrame;
89 import jp.sfjp.jindolf.view.TopView;
90 import jp.sfjp.jindolf.view.WindowManager;
91 import jp.sourceforge.jindolf.corelib.VillageState;
92 import jp.sourceforge.jovsonz.JsObject;
95 * いわゆるMVCでいうとこのコントローラ。
97 public class Controller
98 implements ActionListener,
99 TreeWillExpandListener,
100 TreeSelectionListener,
103 private static final Logger LOGGER = Logger.getAnonymousLogger();
105 private static final String ERRTITLE_LAF = "Look&Feel";
106 private static final String ERRFORM_LAF =
107 "このLook&Feel[{0}]を生成する事ができません。";
109 private final LandsModel model;
110 private final WindowManager windowManager;
111 private final ActionManager actionManager;
112 private final AppSetting appSetting;
114 private final TopView topView;
116 private volatile boolean isBusyNow;
121 * @param model 最上位データモデル
122 * @param windowManager ウィンドウ管理
123 * @param actionManager アクション管理
124 * @param setting アプリ設定
126 @SuppressWarnings("LeakingThisInConstructor")
127 public Controller(LandsModel model,
128 WindowManager windowManager,
129 ActionManager actionManager,
133 this.appSetting = setting;
134 this.actionManager = actionManager;
135 this.windowManager = windowManager;
138 this.topView = this.windowManager.getTopFrame().getTopView();
140 JToolBar toolbar = this.actionManager.getBrowseToolBar();
141 this.topView.setBrowseToolBar(toolbar);
143 this.actionManager.addActionListener(this);
145 JTree treeView = this.topView.getTreeView();
146 treeView.setModel(this.model);
147 treeView.addTreeWillExpandListener(this);
148 treeView.addTreeSelectionListener(this);
150 this.topView.getTabBrowser().addChangeListener(this);
151 this.topView.getTabBrowser().addActionListener(this);
152 this.topView.getTabBrowser().addAnchorHitListener(this);
154 JButton reloadVillageListButton = this.topView
156 .getReloadVillageListButton();
157 reloadVillageListButton.addActionListener(this);
158 reloadVillageListButton.setEnabled(false);
160 TopFrame topFrame = this.windowManager.getTopFrame();
161 TalkPreview talkPreview = this.windowManager.getTalkPreview();
162 OptionPanel optionPanel = this.windowManager.getOptionPanel();
163 FindPanel findPanel = this.windowManager.getFindPanel();
164 FilterPanel filterPanel = this.windowManager.getFilterPanel();
165 LogFrame logFrame = this.windowManager.getLogFrame();
166 AccountPanel accountPanel = this.windowManager.getAccountPanel();
167 HelpFrame helpFrame = this.windowManager.getHelpFrame();
169 topFrame.setJMenuBar(this.actionManager.getMenuBar());
171 topFrame.setDefaultCloseOperation(
172 WindowConstants.DISPOSE_ON_CLOSE);
173 topFrame.addWindowListener(new WindowAdapter(){
175 public void windowClosed(WindowEvent event){
180 filterPanel.addChangeListener(this);
182 Handler newHandler = logFrame.getHandler();
183 LogUtils.switchHandler(newHandler);
185 ConfigStore config = this.appSetting.getConfigStore();
187 JsObject draft = config.loadDraftConfig();
188 talkPreview.putJson(draft);
190 JsObject history = config.loadHistoryConfig();
191 findPanel.putJson(history);
193 FontInfo fontInfo = this.appSetting.getFontInfo();
194 this.topView.getTabBrowser().setFontInfo(fontInfo);
195 talkPreview.setFontInfo(fontInfo);
196 optionPanel.getFontChooser().setFontInfo(fontInfo);
198 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
199 optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
201 DialogPref pref = this.appSetting.getDialogPref();
202 this.topView.getTabBrowser().setDialogPref(pref);
203 optionPanel.getDialogPrefPanel().setDialogPref(pref);
205 accountPanel.setModel(this.model);
207 OptionInfo optInfo = this.appSetting.getOptionInfo();
208 ConfigStore configStore = this.appSetting.getConfigStore();
209 helpFrame.updateVmInfo(optInfo, configStore);
218 public WindowManager getWindowManager(){
219 return this.windowManager;
226 public TopFrame getTopFrame(){
227 TopFrame result = this.windowManager.getTopFrame();
234 * <p>EDT以外から呼ばれると実際の処理が次回のEDT移行に遅延される。
236 * @param isBusy ビジーならtrue
237 * @param message ステータスバー表示。nullなら変更なし
239 public void submitBusyStatus(final boolean isBusy, final String message){
240 Runnable task = new Runnable(){
243 if(isBusy) setBusy(true);
244 if(message != null) updateStatusBar(message);
245 if( ! isBusy ) setBusy(false);
250 EventQueue.invokeLater(task);
258 * <p>タスク実行中はビジー状態となる。
260 * <p>軽量タスク実行中はイベントループが停止するので、
261 * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
264 * @param beforeMsg ビジー中ステータス文字列
265 * @param afterMsg ビジー復帰時のステータス文字列
267 public void submitLightBusyTask(Runnable task,
270 submitBusyStatus(true, beforeMsg);
271 EventQueue.invokeLater(task);
272 submitBusyStatus(false, afterMsg);
280 * <p>タスク実行中はビジー状態となる。
282 * <p>軽量タスク実行中はイベントループが停止するので、
283 * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
285 * <p>タスク終了時、ステータス文字列はタスク実行前の状態に戻る。
288 * @param beforeMsg ビジー中ステータス文字列。
289 * ビジー復帰時は元のステータス文字列に戻る。
291 public void submitLightBusyTask(Runnable task, String beforeMsg){
292 String afterMsg = this.topView.getSysMessage();
293 submitLightBusyTask(task, beforeMsg, afterMsg);
298 * 重量級タスクをEDTとは別のスレッドで実行する。
300 * <p>タスク実行中はビジー状態となる。
302 * @param heavyTask 重量級タスク
303 * @param beforeMsg ビジー中ステータス文字列
304 * @param afterMsg ビジー復帰時のステータス文字列
306 public void submitHeavyBusyTask(final Runnable heavyTask,
307 final String beforeMsg,
308 final String afterMsg ){
309 submitBusyStatus(true, beforeMsg);
311 final Runnable busyManager = new Runnable(){
313 @SuppressWarnings("CallToThreadYield")
319 submitBusyStatus(false, afterMsg);
325 Runnable forkLauncher = new Runnable(){
328 Executor executor = Executors.newCachedThreadPool();
329 executor.execute(busyManager);
334 EventQueue.invokeLater(forkLauncher);
340 * 重量級タスクをEDTとは別のスレッドで実行する。
342 * <p>タスク実行中はビジー状態となる。
344 * <p>タスク終了時、ステータス文字列はタスク実行前の状態に戻る。
347 * @param beforeMsg ビジー中ステータス文字列。
348 * ビジー復帰時は元のステータス文字列に戻る。
350 public void submitHeavyBusyTask(Runnable task, String beforeMsg){
351 String afterMsg = this.topView.getSysMessage();
352 submitHeavyBusyTask(task, beforeMsg, afterMsg);
359 private void actionAbout(){
360 String message = VerInfo.getAboutMessage();
361 JOptionPane pane = new JOptionPane(message,
362 JOptionPane.INFORMATION_MESSAGE,
363 JOptionPane.DEFAULT_OPTION,
364 GUIUtils.getLogoIcon());
366 JDialog dialog = pane.createDialog(getTopFrame(),
367 VerInfo.TITLE + "について");
370 dialog.setVisible(true);
379 private void actionExit(){
387 private void actionHelp(){
388 HelpFrame helpFrame = this.windowManager.getHelpFrame();
389 toggleWindow(helpFrame);
396 private void actionShowWebVillage(){
397 TabBrowser browser = this.topView.getTabBrowser();
398 Village village = browser.getVillage();
399 if(village == null) return;
401 Land land = village.getParentLand();
402 ServerAccess server = land.getServerAccess();
404 URL url = server.getVillageURL(village);
406 String urlText = url.toString();
407 if(village.getState() != VillageState.GAMEOVER){
408 urlText += "#bottom";
411 WebIPCDialog.showDialog(getTopFrame(), urlText);
417 * 村に対応するまとめサイトをWebブラウザで表示する。
419 private void actionShowWebWiki(){
420 TabBrowser browser = this.topView.getTabBrowser();
421 Village village = browser.getVillage();
422 if(village == null) return;
424 String villageName = village.getVillageName();
428 .append("http://wolfbbs.jp/")
430 .append("%C2%BC.html");
432 WebIPCDialog.showDialog(getTopFrame(), url.toString());
438 * 村に対応するキャスト紹介表ジェネレーターをWebブラウザで表示する。
440 private void actionShowWebCast(){
441 TabBrowser browser = this.topView.getTabBrowser();
442 Village village = browser.getVillage();
443 if(village == null) return;
445 Land land = village.getParentLand();
446 ServerAccess server = land.getServerAccess();
448 URL villageUrl = server.getVillageURL(village);
450 StringBuilder url = new StringBuilder("http://hon5.com/jinro/");
454 .append(URLEncoder.encode(villageUrl.toString(), "UTF-8"));
455 }catch(UnsupportedEncodingException e){
461 WebIPCDialog.showDialog(getTopFrame(), url.toString());
467 * 日(Period)をWebブラウザで表示する。
469 private void actionShowWebDay(){
470 PeriodView periodView = currentPeriodView();
471 if(periodView == null) return;
473 Period period = periodView.getPeriod();
474 if(period == null) return;
476 TabBrowser browser = this.topView.getTabBrowser();
477 Village village = browser.getVillage();
478 if(village == null) return;
480 Land land = village.getParentLand();
481 ServerAccess server = land.getServerAccess();
483 URL url = server.getPeriodURL(period);
485 String urlText = url.toString();
486 if(period.isHot()) urlText += "#bottom";
488 WebIPCDialog.showDialog(getTopFrame(), urlText);
494 * 個別の発言をWebブラウザで表示する。
496 private void actionShowWebTalk(){
497 TabBrowser browser = this.topView.getTabBrowser();
498 Village village = browser.getVillage();
499 if(village == null) return;
501 PeriodView periodView = currentPeriodView();
502 if(periodView == null) return;
504 Discussion discussion = periodView.getDiscussion();
505 Talk talk = discussion.getPopupedTalk();
506 if(talk == null) return;
508 Period period = periodView.getPeriod();
509 if(period == null) return;
511 Land land = village.getParentLand();
512 ServerAccess server = land.getServerAccess();
514 URL url = server.getPeriodURL(period);
516 String urlText = url.toString();
517 urlText += "#" + talk.getMessageID();
518 WebIPCDialog.showDialog(getTopFrame(), urlText);
524 * ポータルサイトをWebブラウザで表示する。
526 private void actionShowPortal(){
527 WebIPCDialog.showDialog(getTopFrame(), VerInfo.CONTACT);
532 * 例外発生による警告ダイアログへの反応を促す。
533 * @param title タイトル文字列
534 * @param message メッセージ
537 private void warnDialog(String title, String message, Throwable e){
538 LOGGER.log(Level.WARNING, message, e);
539 JOptionPane.showMessageDialog(
542 VerInfo.getFrameTitle(title),
543 JOptionPane.WARNING_MESSAGE );
550 private void actionChangeLaF(){
551 String className = this.actionManager.getSelectedLookAndFeel();
555 lnfClass = Class.forName(className);
556 }catch(ClassNotFoundException e){
557 String warnMsg = MessageFormat.format(
558 "このLook&Feel[{0}]を読み込む事ができません。",
560 warnDialog(ERRTITLE_LAF, warnMsg, e);
564 final LookAndFeel lnf;
566 lnf = (LookAndFeel) ( lnfClass.newInstance() );
567 }catch(InstantiationException e){
568 String warnMsg = MessageFormat.format(ERRFORM_LAF, className);
569 warnDialog(ERRTITLE_LAF, warnMsg, e);
571 }catch(IllegalAccessException e){
572 String warnMsg = MessageFormat.format(ERRFORM_LAF, className);
573 warnDialog(ERRTITLE_LAF, warnMsg, e);
575 }catch(ClassCastException e){
576 String warnMsg = MessageFormat.format(ERRFORM_LAF, className);
577 warnDialog(ERRTITLE_LAF, warnMsg, e);
581 Runnable lafTask = new Runnable(){
589 submitLightBusyTask(lafTask,
591 "Look&Feelが更新されました" );
597 * LookAndFeelの実際の更新を行う。
598 * @param lnf LookAndFeel
600 private void changeLaF(LookAndFeel lnf){
601 assert EventQueue.isDispatchThread();
604 UIManager.setLookAndFeel(lnf);
605 }catch(UnsupportedLookAndFeelException e){
606 String warnMsg = MessageFormat.format(
607 "このLook&Feel[{0}]はサポートされていません。",
609 warnDialog(ERRTITLE_LAF, warnMsg, e);
613 this.windowManager.changeAllWindowUI();
615 LOGGER.log(Level.INFO,
616 "Look&Feelが[{0}]に変更されました。", lnf.getName() );
624 private void actionShowFilter(){
625 FilterPanel filterPanel = this.windowManager.getFilterPanel();
626 toggleWindow(filterPanel);
633 private void actionShowAccount(){
634 AccountPanel accountPanel = this.windowManager.getAccountPanel();
635 toggleWindow(accountPanel);
642 private void actionShowLog(){
643 LogFrame logFrame = this.windowManager.getLogFrame();
644 toggleWindow(logFrame);
651 private void actionTalkPreview(){
652 TalkPreview talkPreview = this.windowManager.getTalkPreview();
653 toggleWindow(talkPreview);
660 private void actionOption(){
661 OptionPanel optionPanel = this.windowManager.getOptionPanel();
663 FontInfo fontInfo = this.appSetting.getFontInfo();
664 optionPanel.getFontChooser().setFontInfo(fontInfo);
666 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
667 optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
669 DialogPref dialogPref = this.appSetting.getDialogPref();
670 optionPanel.getDialogPrefPanel().setDialogPref(dialogPref);
672 optionPanel.setVisible(true);
673 if(optionPanel.isCanceled()) return;
675 fontInfo = optionPanel.getFontChooser().getFontInfo();
676 updateFontInfo(fontInfo);
678 proxyInfo = optionPanel.getProxyChooser().getProxyInfo();
679 updateProxyInfo(proxyInfo);
681 dialogPref = optionPanel.getDialogPrefPanel().getDialogPref();
682 updateDialogPref(dialogPref);
689 * @param newFontInfo 新フォント設定
691 private void updateFontInfo(final FontInfo newFontInfo){
692 FontInfo oldInfo = this.appSetting.getFontInfo();
694 if(newFontInfo.equals(oldInfo)) return;
695 this.appSetting.setFontInfo(newFontInfo);
697 this.topView.getTabBrowser().setFontInfo(newFontInfo);
699 TalkPreview talkPreview = this.windowManager.getTalkPreview();
700 OptionPanel optionPanel = this.windowManager.getOptionPanel();
701 FontChooser fontChooser = optionPanel.getFontChooser();
703 talkPreview.setFontInfo(newFontInfo);
704 fontChooser.setFontInfo(newFontInfo);
711 * @param newProxyInfo 新プロクシ設定
713 private void updateProxyInfo(ProxyInfo newProxyInfo){
714 ProxyInfo oldProxyInfo = this.appSetting.getProxyInfo();
716 if(newProxyInfo.equals(oldProxyInfo)) return;
717 this.appSetting.setProxyInfo(newProxyInfo);
719 for(Land land : this.model.getLandList()){
720 ServerAccess server = land.getServerAccess();
721 server.setProxy(newProxyInfo.getProxy());
729 * @param newDialogPref 表示設定
731 private void updateDialogPref(DialogPref newDialogPref){
732 DialogPref oldDialogPref = this.appSetting.getDialogPref();
734 if(newDialogPref.equals(oldDialogPref)) return;
735 this.appSetting.setDialogPref(newDialogPref);
737 this.topView.getTabBrowser().setDialogPref(newDialogPref);
745 private void actionShowDigest(){
746 TabBrowser browser = this.topView.getTabBrowser();
747 final Village village = browser.getVillage();
748 if(village == null) return;
750 VillageState villageState = village.getState();
751 if( ( villageState != VillageState.EPILOGUE
752 && villageState != VillageState.GAMEOVER
753 ) || ! village.isValid() ){
754 String message = "エピローグを正常に迎えていない村は\n"
756 String title = VerInfo.getFrameTitle("ダイジェスト不可");
757 JOptionPane pane = new JOptionPane(message,
758 JOptionPane.WARNING_MESSAGE,
759 JOptionPane.DEFAULT_OPTION );
760 JDialog dialog = pane.createDialog(getTopFrame(), title);
762 dialog.setVisible(true);
767 VillageDigest villageDigest = this.windowManager.getVillageDigest();
768 final VillageDigest digest = villageDigest;
769 Executor executor = Executors.newCachedThreadPool();
770 executor.execute(new Runnable(){
773 taskFullOpenAllPeriod();
774 EventQueue.invokeLater(new Runnable(){
777 digest.setVillage(village);
778 digest.setVisible(true);
790 * 全日程の一括フルオープン。ヘビータスク版。
792 // TODO taskLoadAllPeriodtと一体化したい。
793 private void taskFullOpenAllPeriod(){
795 updateStatusBar("一括読み込み開始");
797 TabBrowser browser = this.topView.getTabBrowser();
798 Village village = browser.getVillage();
799 if(village == null) return;
800 for(PeriodView periodView : browser.getPeriodViewList()){
801 Period period = periodView.getPeriod();
802 if(period == null) continue;
803 if(period.isFullOpen()) continue;
807 updateStatusBar(message);
809 Period.parsePeriod(period, true);
810 }catch(IOException e){
811 showNetworkError(village, e);
814 periodView.showTopics();
817 updateStatusBar("一括読み込み完了");
826 private void actionShowFind(){
827 FindPanel findPanel = this.windowManager.getFindPanel();
829 findPanel.setVisible(true);
830 if(findPanel.isCanceled()){
834 if(findPanel.isBulkSearch()){
845 private void regexSearch(){
846 Discussion discussion = currentDiscussion();
847 if(discussion == null) return;
849 FindPanel findPanel = this.windowManager.getFindPanel();
850 RegexPattern regPattern = findPanel.getRegexPattern();
851 int hits = discussion.setRegexPattern(regPattern);
853 String hitMessage = "[" + hits + "]件ヒットしました";
854 updateStatusBar(hitMessage);
857 if(regPattern != null){
858 Pattern pattern = regPattern.getPattern();
860 loginfo = "正規表現 " + pattern.pattern() + " に";
863 loginfo += hitMessage;
864 LOGGER.info(loginfo);
872 private void bulkSearch(){
873 Executor executor = Executors.newCachedThreadPool();
874 executor.execute(new Runnable(){
886 private void taskBulkSearch(){
889 FindPanel findPanel = this.windowManager.getFindPanel();
890 RegexPattern regPattern = findPanel.getRegexPattern();
891 StringBuilder hitDesc = new StringBuilder();
892 TabBrowser browser = this.topView.getTabBrowser();
893 for(PeriodView periodView : browser.getPeriodViewList()){
894 Discussion discussion = periodView.getDiscussion();
895 int hits = discussion.setRegexPattern(regPattern);
899 Period period = discussion.getPeriod();
900 hitDesc.append(' ').append(period.getDay()).append("d:");
901 hitDesc.append(hits).append("件");
905 "[" + totalhits + "]件ヒットしました。"
906 + hitDesc.toString();
907 updateStatusBar(hitMessage);
910 if(regPattern != null){
911 Pattern pattern = regPattern.getPattern();
913 loginfo = "正規表現 " + pattern.pattern() + " に";
916 loginfo += hitMessage;
917 LOGGER.info(loginfo);
923 * 検索パネルに現在選択中のPeriodを反映させる。
925 private void updateFindPanel(){
926 Discussion discussion = currentDiscussion();
927 if(discussion == null) return;
928 RegexPattern pattern = discussion.getRegexPattern();
929 FindPanel findPanel = this.windowManager.getFindPanel();
930 findPanel.setRegexPattern(pattern);
937 private void actionDaySummary(){
938 PeriodView periodView = currentPeriodView();
939 if(periodView == null) return;
941 Period period = periodView.getPeriod();
942 if(period == null) return;
944 DaySummary daySummary = this.windowManager.getDaySummary();
945 daySummary.summaryPeriod(period);
946 daySummary.setVisible(true);
952 * 表示中PeriodをCSVファイルへエクスポートする。
954 private void actionDayExportCsv(){
955 PeriodView periodView = currentPeriodView();
956 if(periodView == null) return;
958 Period period = periodView.getPeriod();
959 if(period == null) return;
961 FilterPanel filterPanel = this.windowManager.getFilterPanel();
962 File file = CsvExporter.exportPeriod(period, filterPanel);
964 String message = "CSVファイル("
967 updateStatusBar(message);
970 // TODO 長そうなジョブなら別スレッドにした方がいいか?
978 private void actionSearchNext(){
979 Discussion discussion = currentDiscussion();
980 if(discussion == null) return;
982 discussion.nextHotTarget();
990 private void actionSearchPrev(){
991 Discussion discussion = currentDiscussion();
992 if(discussion == null) return;
994 discussion.prevHotTarget();
1002 private void actionReloadPeriod(){
1005 TabBrowser tabBrowser = this.topView.getTabBrowser();
1006 Village village = tabBrowser.getVillage();
1007 if(village == null) return;
1008 if(village.getState() != VillageState.EPILOGUE) return;
1010 Discussion discussion = currentDiscussion();
1011 if(discussion == null) return;
1012 Period period = discussion.getPeriod();
1013 if(period == null) return;
1014 if(period.getTopics() > 1000){
1015 JOptionPane.showMessageDialog(getTopFrame(),
1016 "エピローグが1000発言を超えはじめたら、\n"
1017 +"負荷対策のためWebブラウザによるアクセスを"
1020 JOptionPane.WARNING_MESSAGE
1030 private void actionLoadAllPeriod(){
1031 Executor executor = Executors.newCachedThreadPool();
1032 executor.execute(new Runnable(){
1035 taskLoadAllPeriod();
1044 * 全日程の一括ロード。ヘビータスク版。
1046 private void taskLoadAllPeriod(){
1048 updateStatusBar("一括読み込み開始");
1050 TabBrowser browser = this.topView.getTabBrowser();
1051 Village village = browser.getVillage();
1052 if(village == null) return;
1053 for(PeriodView periodView : browser.getPeriodViewList()){
1054 Period period = periodView.getPeriod();
1055 if(period == null) continue;
1058 + "日目のデータを読み込んでいます";
1059 updateStatusBar(message);
1061 Period.parsePeriod(period, false);
1062 }catch(IOException e){
1063 showNetworkError(village, e);
1066 periodView.showTopics();
1069 updateStatusBar("一括読み込み完了");
1078 private void actionReloadVillageList(){
1079 JTree tree = this.topView.getTreeView();
1080 TreePath path = tree.getSelectionPath();
1081 if(path == null) return;
1084 for(int ct = 0; ct < path.getPathCount(); ct++){
1085 Object obj = path.getPathComponent(ct);
1086 if(obj instanceof Land){
1091 if(land == null) return;
1093 this.topView.showInitPanel();
1095 submitReloadVillageList(land);
1101 * 選択文字列をクリップボードにコピーする。
1103 private void actionCopySelected(){
1104 Discussion discussion = currentDiscussion();
1105 if(discussion == null) return;
1107 CharSequence copied = discussion.copySelected();
1108 if(copied == null) return;
1110 copied = StringUtils.suppressString(copied);
1112 "[" + copied + "]をクリップボードにコピーしました");
1117 * 一発言のみクリップボードにコピーする。
1119 private void actionCopyTalk(){
1120 Discussion discussion = currentDiscussion();
1121 if(discussion == null) return;
1123 CharSequence copied = discussion.copyTalk();
1124 if(copied == null) return;
1126 copied = StringUtils.suppressString(copied);
1128 "[" + copied + "]をクリップボードにコピーしました");
1135 private void actionJumpAnchor(){
1136 PeriodView periodView = currentPeriodView();
1137 if(periodView == null) return;
1138 Discussion discussion = periodView.getDiscussion();
1140 final TabBrowser browser = this.topView.getTabBrowser();
1141 final Village village = browser.getVillage();
1142 final Anchor anchor = discussion.getPopupedAnchor();
1143 if(anchor == null) return;
1145 Executor executor = Executors.newCachedThreadPool();
1146 executor.execute(new Runnable(){
1150 updateStatusBar("ジャンプ先の読み込み中…");
1152 if(anchor.hasTalkNo()){
1154 taskLoadAllPeriod();
1157 final List<Talk> talkList;
1159 talkList = village.getTalkListFromAnchor(anchor);
1160 if(talkList == null || talkList.size() <= 0){
1168 final Talk targetTalk = talkList.get(0);
1169 final Period targetPeriod = targetTalk.getPeriod();
1170 final int tabIndex = targetPeriod.getDay() + 1;
1171 final PeriodView target = browser.getPeriodView(tabIndex);
1173 EventQueue.invokeLater(new Runnable(){
1176 browser.setSelectedIndex(tabIndex);
1177 target.setPeriod(targetPeriod);
1178 target.scrollToTalk(targetTalk);
1186 }catch(IOException e){
1188 "アンカーの展開中にエラーが起きました");
1201 * 指定した国の村一覧を読み込むジョブを投下。
1204 private void submitReloadVillageList(final Land land){
1205 Runnable heavyTask = new Runnable(){
1208 taskReloadVillageList(land);
1213 submitHeavyBusyTask(heavyTask,
1221 * 指定した国の村一覧を読み込む。(ヘビータスク本体).
1224 private void taskReloadVillageList(Land land){
1225 SortedSet<Village> villageList;
1227 villageList = land.downloadVillageList();
1228 }catch(IOException e){
1229 showNetworkError(land, e);
1232 land.updateVillageList(villageList);
1234 this.model.updateVillageList(land);
1236 LandsTree treePanel = this.topView.getLandsTree();
1237 treePanel.expandLand(land);
1244 * @param force trueならPeriodデータを強制再読み込み。
1246 private void updatePeriod(final boolean force){
1247 final TabBrowser tabBrowser = this.topView.getTabBrowser();
1248 final Village village = tabBrowser.getVillage();
1249 if(village == null) return;
1250 setFrameTitle(village.getVillageFullName());
1252 final PeriodView periodView = currentPeriodView();
1253 Discussion discussion = currentDiscussion();
1254 if(discussion == null) return;
1255 FilterPanel filterPanel = this.windowManager.getFilterPanel();
1256 discussion.setTopicFilter(filterPanel);
1257 final Period period = discussion.getPeriod();
1258 if(period == null) return;
1260 Executor executor = Executors.newCachedThreadPool();
1261 executor.execute(new Runnable(){
1266 boolean wasHot = loadPeriod();
1268 if(wasHot && ! period.isHot() ){
1269 if( ! updatePeriodList() ) return;
1279 private boolean loadPeriod(){
1280 updateStatusBar("1日分のデータを読み込んでいます…");
1283 wasHot = period.isHot();
1285 Period.parsePeriod(period, force);
1286 }catch(IOException e){
1287 showNetworkError(village, e);
1290 updateStatusBar("1日分のデータを読み終わりました");
1295 private boolean updatePeriodList(){
1296 updateStatusBar("村情報を読み直しています…");
1298 Village.updateVillage(village);
1299 }catch(IOException e){
1300 showNetworkError(village, e);
1304 SwingUtilities.invokeAndWait(new Runnable(){
1307 tabBrowser.setVillage(village);
1311 }catch(InvocationTargetException e){
1312 LOGGER.log(Level.SEVERE,
1313 "タブ操作で致命的な障害が発生しました", e);
1314 }catch(InterruptedException e){
1315 LOGGER.log(Level.SEVERE,
1316 "タブ操作で致命的な障害が発生しました", e);
1318 updateStatusBar("村情報を読み直しました…");
1322 private void renderBrowser(){
1323 updateStatusBar("レンダリング中…");
1325 final int lastPos = periodView.getVerticalPosition();
1327 SwingUtilities.invokeAndWait(new Runnable(){
1330 periodView.showTopics();
1334 }catch(InvocationTargetException e){
1335 LOGGER.log(Level.SEVERE,
1336 "ブラウザ表示で致命的な障害が発生しました",
1338 }catch(InterruptedException e){
1339 LOGGER.log(Level.SEVERE,
1340 "ブラウザ表示で致命的な障害が発生しました",
1343 EventQueue.invokeLater(new Runnable(){
1346 periodView.setVerticalPosition(lastPos);
1350 updateStatusBar("レンダリング完了");
1362 private void filterChanged(){
1363 final Discussion discussion = currentDiscussion();
1364 if(discussion == null) return;
1366 FilterPanel filterPanel = this.windowManager.getFilterPanel();
1368 discussion.setTopicFilter(filterPanel);
1369 discussion.filtering();
1375 * 現在選択中のPeriodを内包するPeriodViewを返す。
1376 * @return PeriodView
1378 private PeriodView currentPeriodView(){
1379 TabBrowser tb = this.topView.getTabBrowser();
1380 PeriodView result = tb.currentPeriodView();
1385 * 現在選択中のPeriodを内包するDiscussionを返す。
1386 * @return Discussion
1388 private Discussion currentDiscussion(){
1389 PeriodView periodView = currentPeriodView();
1390 if(periodView == null) return null;
1391 Discussion result = periodView.getDiscussion();
1397 * @param window フレーム
1399 private void toggleWindow(Window window){
1400 if(window == null) return;
1402 if(window instanceof Frame){
1403 Frame frame = (Frame) window;
1404 int winState = frame.getExtendedState();
1405 boolean isIconified = (winState & Frame.ICONIFIED) != 0;
1407 winState &= ~(Frame.ICONIFIED);
1408 frame.setExtendedState(winState);
1409 frame.setVisible(true);
1414 if(window.isVisible()){
1415 window.setVisible(false);
1418 window.setVisible(true);
1424 * ネットワークエラーを通知するモーダルダイアログを表示する。
1425 * OKボタンを押すまでこのメソッドは戻ってこない。
1427 * @param e ネットワークエラー
1429 public void showNetworkError(Village village, IOException e){
1430 Land land = village.getParentLand();
1431 showNetworkError(land, e);
1436 * ネットワークエラーを通知するモーダルダイアログを表示する。
1437 * OKボタンを押すまでこのメソッドは戻ってこない。
1439 * @param e ネットワークエラー
1441 public void showNetworkError(Land land, IOException e){
1442 LOGGER.log(Level.WARNING, "ネットワークで障害が発生しました", e);
1444 ServerAccess server = land.getServerAccess();
1446 land.getLandDef().getLandName()
1448 +"何らかのトラブルが発生しました。\n"
1449 +"相手サーバのURLは [ " + server.getBaseURL() + " ] だよ。\n"
1451 +"Webブラウザでも遊べないか確認してみてね!\n";
1453 JOptionPane pane = new JOptionPane(message,
1454 JOptionPane.WARNING_MESSAGE,
1455 JOptionPane.DEFAULT_OPTION );
1457 String title = VerInfo.getFrameTitle("通信異常発生");
1458 JDialog dialog = pane.createDialog(getTopFrame(), title);
1461 dialog.setVisible(true);
1469 * ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
1470 * @param event イベント {@inheritDoc}
1473 public void valueChanged(TreeSelectionEvent event){
1474 TreePath path = event.getNewLeadSelectionPath();
1475 if(path == null) return;
1477 Object selObj = path.getLastPathComponent();
1479 if( selObj instanceof Land ){
1480 Land land = (Land) selObj;
1481 setFrameTitle(land.getLandDef().getLandName());
1482 this.topView.showLandInfo(land);
1483 this.actionManager.appearVillage(false);
1484 this.actionManager.appearPeriod(false);
1485 }else if( selObj instanceof Village ){
1486 final Village village = (Village) selObj;
1488 Executor executor = Executors.newCachedThreadPool();
1489 executor.execute(new Runnable(){
1493 updateStatusBar("村情報を読み込み中…");
1496 Village.updateVillage(village);
1497 }catch(IOException e){
1498 showNetworkError(village, e);
1501 updateStatusBar("村情報の読み込み完了");
1505 Controller.this.actionManager.appearVillage(true);
1506 setFrameTitle(village.getVillageFullName());
1508 EventQueue.invokeLater(new Runnable(){
1511 Controller.this.topView.showVillageInfo(village);
1526 * Periodがタブ選択されたときもしくは発言フィルタが操作されたときの処理。
1527 * @param event イベント {@inheritDoc}
1530 public void stateChanged(ChangeEvent event){
1531 Object source = event.getSource();
1533 if(source == this.windowManager.getFilterPanel()){
1535 }else if(source instanceof TabBrowser){
1537 updatePeriod(false);
1538 PeriodView periodView = currentPeriodView();
1539 if(periodView == null) this.actionManager.appearPeriod(false);
1540 else this.actionManager.appearPeriod(true);
1548 * @param e イベント {@inheritDoc}
1551 public void actionPerformed(ActionEvent e){
1552 if(this.isBusyNow) return;
1554 String cmd = e.getActionCommand();
1555 if(cmd.equals(ActionManager.CMD_ACCOUNT)){
1556 actionShowAccount();
1557 }else if(cmd.equals(ActionManager.CMD_EXIT)){
1559 }else if(cmd.equals(ActionManager.CMD_COPY)){
1560 actionCopySelected();
1561 }else if(cmd.equals(ActionManager.CMD_SHOWFIND)){
1563 }else if(cmd.equals(ActionManager.CMD_SEARCHNEXT)){
1565 }else if(cmd.equals(ActionManager.CMD_SEARCHPREV)){
1567 }else if(cmd.equals(ActionManager.CMD_ALLPERIOD)){
1568 actionLoadAllPeriod();
1569 }else if(cmd.equals(ActionManager.CMD_SHOWDIGEST)){
1571 }else if(cmd.equals(ActionManager.CMD_WEBVILL)){
1572 actionShowWebVillage();
1573 }else if(cmd.equals(ActionManager.CMD_WEBWIKI)){
1574 actionShowWebWiki();
1575 }else if(cmd.equals(ActionManager.CMD_WEBCAST)){
1576 actionShowWebCast();
1577 }else if(cmd.equals(ActionManager.CMD_RELOAD)){
1578 actionReloadPeriod();
1579 }else if(cmd.equals(ActionManager.CMD_DAYSUMMARY)){
1581 }else if(cmd.equals(ActionManager.CMD_DAYEXPCSV)){
1582 actionDayExportCsv();
1583 }else if(cmd.equals(ActionManager.CMD_WEBDAY)){
1585 }else if(cmd.equals(ActionManager.CMD_OPTION)){
1587 }else if(cmd.equals(ActionManager.CMD_LANDF)){
1589 }else if(cmd.equals(ActionManager.CMD_SHOWFILT)){
1591 }else if(cmd.equals(ActionManager.CMD_SHOWEDIT)){
1592 actionTalkPreview();
1593 }else if(cmd.equals(ActionManager.CMD_SHOWLOG)){
1595 }else if(cmd.equals(ActionManager.CMD_HELPDOC)){
1597 }else if(cmd.equals(ActionManager.CMD_SHOWPORTAL)){
1599 }else if(cmd.equals(ActionManager.CMD_ABOUT)){
1601 }else if(cmd.equals(ActionManager.CMD_VILLAGELIST)){
1602 actionReloadVillageList();
1603 }else if(cmd.equals(ActionManager.CMD_COPYTALK)){
1605 }else if(cmd.equals(ActionManager.CMD_JUMPANCHOR)){
1607 }else if(cmd.equals(ActionManager.CMD_WEBTALK)){
1608 actionShowWebTalk();
1615 * 村選択ツリーリストが畳まれるとき呼ばれる。
1616 * @param event ツリーイベント {@inheritDoc}
1619 public void treeWillCollapse(TreeExpansionEvent event){
1625 * 村選択ツリーリストが展開されるとき呼ばれる。
1626 * @param event ツリーイベント {@inheritDoc}
1629 public void treeWillExpand(TreeExpansionEvent event){
1630 if(!(event.getSource() instanceof JTree)){
1634 TreePath path = event.getPath();
1635 Object lastObj = path.getLastPathComponent();
1636 if(!(lastObj instanceof Land)){
1639 final Land land = (Land) lastObj;
1640 if(land.getVillageCount() > 0){
1644 submitReloadVillageList(land);
1651 * @param event {@inheritDoc}
1654 public void anchorHitted(AnchorHitEvent event){
1655 PeriodView periodView = currentPeriodView();
1656 if(periodView == null) return;
1657 Period period = periodView.getPeriod();
1658 if(period == null) return;
1659 final Village village = period.getVillage();
1661 final TalkDraw talkDraw = event.getTalkDraw();
1662 final Anchor anchor = event.getAnchor();
1663 final Discussion discussion = periodView.getDiscussion();
1665 Executor executor = Executors.newCachedThreadPool();
1666 executor.execute(new Runnable(){
1670 updateStatusBar("アンカーの展開中…");
1672 if(anchor.hasTalkNo()){
1674 taskLoadAllPeriod();
1677 final List<Talk> talkList;
1679 talkList = village.getTalkListFromAnchor(anchor);
1680 if(talkList == null || talkList.size() <= 0){
1687 EventQueue.invokeLater(new Runnable(){
1690 talkDraw.showAnchorTalks(anchor, talkList);
1691 discussion.layoutRows();
1699 }catch(IOException e){
1701 "アンカーの展開中にエラーが起きました");
1715 * プログレスバーとカーソルの設定を行う。
1716 * @param isBusy trueならプログレスバーのアニメ開始&WAITカーソル。
1719 private void setBusy(final boolean isBusy){
1720 this.isBusyNow = isBusy;
1722 Runnable microJob = new Runnable(){
1727 cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
1729 cursor = Cursor.getDefaultCursor();
1732 Component glass = getTopFrame().getGlassPane();
1733 glass.setCursor(cursor);
1734 glass.setVisible(isBusy);
1735 Controller.this.topView.setBusy(isBusy);
1741 if(SwingUtilities.isEventDispatchThread()){
1745 SwingUtilities.invokeAndWait(microJob);
1746 }catch(InvocationTargetException e){
1747 LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
1748 }catch(InterruptedException e){
1749 LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
1758 * @param message メッセージ
1760 private void updateStatusBar(String message){
1761 this.topView.updateSysMessage(message);
1765 * トップフレームのタイトルを設定する。
1766 * タイトルは指定された国or村名 + " - Jindolf"
1769 private void setFrameTitle(String name){
1770 String title = VerInfo.getFrameTitle(name);
1771 TopFrame topFrame = this.windowManager.getTopFrame();
1772 topFrame.setTitle(title);
1779 private void shutdown(){
1780 ConfigStore configStore = this.appSetting.getConfigStore();
1782 FindPanel findPanel = this.windowManager.getFindPanel();
1783 JsObject findConf = findPanel.getJson();
1784 if( ! findPanel.hasConfChanged(findConf) ){
1785 configStore.saveHistoryConfig(findConf);
1788 TalkPreview talkPreview = this.windowManager.getTalkPreview();
1789 JsObject draftConf = talkPreview.getJson();
1790 if( ! talkPreview.hasConfChanged(draftConf) ){
1791 configStore.saveDraftConfig(draftConf);
1794 this.appSetting.saveConfig();
1796 LOGGER.info("VMごとアプリケーションを終了します。");
1797 System.exit(0); // invoke shutdown hooks... BYE !