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.lang.reflect.InvocationTargetException;
23 import java.text.MessageFormat;
24 import java.util.List;
25 import java.util.SortedSet;
26 import java.util.concurrent.Executor;
27 import java.util.concurrent.Executors;
28 import java.util.logging.Handler;
29 import java.util.logging.Level;
30 import java.util.logging.Logger;
31 import java.util.regex.Pattern;
32 import javax.swing.JButton;
33 import javax.swing.JDialog;
34 import javax.swing.JOptionPane;
35 import javax.swing.JToolBar;
36 import javax.swing.JTree;
37 import javax.swing.LookAndFeel;
38 import javax.swing.SwingUtilities;
39 import javax.swing.UIManager;
40 import javax.swing.UnsupportedLookAndFeelException;
41 import javax.swing.WindowConstants;
42 import javax.swing.event.ChangeEvent;
43 import javax.swing.event.ChangeListener;
44 import javax.swing.event.TreeExpansionEvent;
45 import javax.swing.event.TreeSelectionEvent;
46 import javax.swing.event.TreeSelectionListener;
47 import javax.swing.event.TreeWillExpandListener;
48 import javax.swing.tree.TreePath;
49 import jp.sfjp.jindolf.config.AppSetting;
50 import jp.sfjp.jindolf.config.ConfigStore;
51 import jp.sfjp.jindolf.config.OptionInfo;
52 import jp.sfjp.jindolf.data.Anchor;
53 import jp.sfjp.jindolf.data.DialogPref;
54 import jp.sfjp.jindolf.data.Land;
55 import jp.sfjp.jindolf.data.LandsModel;
56 import jp.sfjp.jindolf.data.Period;
57 import jp.sfjp.jindolf.data.RegexPattern;
58 import jp.sfjp.jindolf.data.Talk;
59 import jp.sfjp.jindolf.data.Village;
60 import jp.sfjp.jindolf.dxchg.CsvExporter;
61 import jp.sfjp.jindolf.dxchg.WebIPCDialog;
62 import jp.sfjp.jindolf.dxchg.WolfBBS;
63 import jp.sfjp.jindolf.editor.TalkPreview;
64 import jp.sfjp.jindolf.glyph.AnchorHitEvent;
65 import jp.sfjp.jindolf.glyph.AnchorHitListener;
66 import jp.sfjp.jindolf.glyph.Discussion;
67 import jp.sfjp.jindolf.glyph.FontChooser;
68 import jp.sfjp.jindolf.glyph.FontInfo;
69 import jp.sfjp.jindolf.glyph.TalkDraw;
70 import jp.sfjp.jindolf.log.LogFrame;
71 import jp.sfjp.jindolf.log.LogUtils;
72 import jp.sfjp.jindolf.net.ProxyInfo;
73 import jp.sfjp.jindolf.net.ServerAccess;
74 import jp.sfjp.jindolf.summary.DaySummary;
75 import jp.sfjp.jindolf.summary.VillageDigest;
76 import jp.sfjp.jindolf.util.GUIUtils;
77 import jp.sfjp.jindolf.util.StringUtils;
78 import jp.sfjp.jindolf.view.AccountPanel;
79 import jp.sfjp.jindolf.view.ActionManager;
80 import jp.sfjp.jindolf.view.FilterPanel;
81 import jp.sfjp.jindolf.view.FindPanel;
82 import jp.sfjp.jindolf.view.HelpFrame;
83 import jp.sfjp.jindolf.view.LandsTree;
84 import jp.sfjp.jindolf.view.OptionPanel;
85 import jp.sfjp.jindolf.view.PeriodView;
86 import jp.sfjp.jindolf.view.TabBrowser;
87 import jp.sfjp.jindolf.view.TopFrame;
88 import jp.sfjp.jindolf.view.TopView;
89 import jp.sfjp.jindolf.view.WindowManager;
90 import jp.sourceforge.jindolf.corelib.VillageState;
91 import jp.sourceforge.jovsonz.JsObject;
94 * いわゆるMVCでいうとこのコントローラ。
96 public class Controller
97 implements ActionListener,
98 TreeWillExpandListener,
99 TreeSelectionListener,
102 private static final Logger LOGGER = Logger.getAnonymousLogger();
104 private static final String ERRTITLE_LAF = "Look&Feel";
105 private static final String ERRFORM_LAF =
106 "この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(){
176 public void windowClosed(WindowEvent event){
181 filterPanel.addChangeListener(this);
183 Handler newHandler = logFrame.getHandler();
184 LogUtils.switchHandler(newHandler);
186 ConfigStore config = this.appSetting.getConfigStore();
188 JsObject draft = config.loadDraftConfig();
189 talkPreview.putJson(draft);
191 JsObject history = config.loadHistoryConfig();
192 findPanel.putJson(history);
194 FontInfo fontInfo = this.appSetting.getFontInfo();
195 this.topView.getTabBrowser().setFontInfo(fontInfo);
196 talkPreview.setFontInfo(fontInfo);
197 optionPanel.getFontChooser().setFontInfo(fontInfo);
199 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
200 optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
202 DialogPref pref = this.appSetting.getDialogPref();
203 this.topView.getTabBrowser().setDialogPref(pref);
204 optionPanel.getDialogPrefPanel().setDialogPref(pref);
206 accountPanel.setModel(this.model);
208 OptionInfo optInfo = this.appSetting.getOptionInfo();
209 ConfigStore configStore = this.appSetting.getConfigStore();
210 helpFrame.updateVmInfo(optInfo, configStore);
219 public WindowManager getWindowManager(){
220 return this.windowManager;
227 public TopFrame getTopFrame(){
228 TopFrame result = this.windowManager.getTopFrame();
235 * <p>EDT以外から呼ばれると実際の処理が次回のEDT移行に遅延される。
237 * @param isBusy ビジーならtrue
238 * @param message ステータスバー表示。nullなら変更なし
240 public void submitBusyStatus(final boolean isBusy, final String message){
241 Runnable task = new Runnable(){
245 if(isBusy) setBusy(true);
246 if(message != null) updateStatusBar(message);
247 if( ! isBusy ) setBusy(false);
252 EventQueue.invokeLater(task);
260 * <p>タスク実行中はビジー状態となる。
262 * <p>軽量タスク実行中はイベントループが停止するので、
263 * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
266 * @param beforeMsg ビジー中ステータス文字列
267 * @param afterMsg ビジー復帰時のステータス文字列
269 public void submitLightBusyTask(Runnable task,
272 submitBusyStatus(true, beforeMsg);
273 EventQueue.invokeLater(task);
274 submitBusyStatus(false, afterMsg);
282 * <p>タスク実行中はビジー状態となる。
284 * <p>軽量タスク実行中はイベントループが停止するので、
285 * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
287 * <p>タスク終了時、ステータス文字列はタスク実行前の状態に戻る。
290 * @param beforeMsg ビジー中ステータス文字列。
291 * ビジー復帰時は元のステータス文字列に戻る。
293 public void submitLightBusyTask(Runnable task, String beforeMsg){
294 String afterMsg = this.topView.getSysMessage();
295 submitLightBusyTask(task, beforeMsg, afterMsg);
300 * 重量級タスクをEDTとは別のスレッドで実行する。
302 * <p>タスク実行中はビジー状態となる。
304 * @param heavyTask 重量級タスク
305 * @param beforeMsg ビジー中ステータス文字列
306 * @param afterMsg ビジー復帰時のステータス文字列
308 public void submitHeavyBusyTask(final Runnable heavyTask,
309 final String beforeMsg,
310 final String afterMsg ){
311 submitBusyStatus(true, beforeMsg);
313 final Runnable busyManager = new Runnable(){
316 @SuppressWarnings("CallToThreadYield")
322 submitBusyStatus(false, afterMsg);
328 Runnable forkLauncher = new Runnable(){
332 Executor executor = Executors.newCachedThreadPool();
333 executor.execute(busyManager);
338 EventQueue.invokeLater(forkLauncher);
344 * 重量級タスクをEDTとは別のスレッドで実行する。
346 * <p>タスク実行中はビジー状態となる。
348 * <p>タスク終了時、ステータス文字列はタスク実行前の状態に戻る。
351 * @param beforeMsg ビジー中ステータス文字列。
352 * ビジー復帰時は元のステータス文字列に戻る。
354 public void submitHeavyBusyTask(Runnable task, String beforeMsg){
355 String afterMsg = this.topView.getSysMessage();
356 submitHeavyBusyTask(task, beforeMsg, afterMsg);
363 private void actionAbout(){
364 String message = VerInfo.getAboutMessage();
365 JOptionPane pane = new JOptionPane(message,
366 JOptionPane.INFORMATION_MESSAGE,
367 JOptionPane.DEFAULT_OPTION,
368 GUIUtils.getLogoIcon());
370 JDialog dialog = pane.createDialog(getTopFrame(),
371 VerInfo.TITLE + "について");
374 dialog.setVisible(true);
383 private void actionExit(){
391 private void actionHelp(){
392 HelpFrame helpFrame = this.windowManager.getHelpFrame();
393 toggleWindow(helpFrame);
400 private void actionShowWebVillage(){
401 TabBrowser browser = this.topView.getTabBrowser();
402 Village village = browser.getVillage();
403 if(village == null) return;
405 Land land = village.getParentLand();
406 ServerAccess server = land.getServerAccess();
408 URL url = server.getVillageURL(village);
410 String urlText = url.toString();
411 if(village.getState() != VillageState.GAMEOVER){
412 urlText += "#bottom";
415 WebIPCDialog.showDialog(getTopFrame(), urlText);
421 * 村に対応するまとめサイトをWebブラウザで表示する。
423 private void actionShowWebWiki(){
424 TabBrowser browser = this.topView.getTabBrowser();
425 Village village = browser.getVillage();
426 if(village == null) return;
428 String urlTxt = WolfBBS.getCastGeneratorUrl(village);
429 WebIPCDialog.showDialog(getTopFrame(), urlTxt);
435 * 日(Period)をWebブラウザで表示する。
437 private void actionShowWebDay(){
438 PeriodView periodView = currentPeriodView();
439 if(periodView == null) return;
441 Period period = periodView.getPeriod();
442 if(period == null) return;
444 TabBrowser browser = this.topView.getTabBrowser();
445 Village village = browser.getVillage();
446 if(village == null) return;
448 Land land = village.getParentLand();
449 ServerAccess server = land.getServerAccess();
451 URL url = server.getPeriodURL(period);
453 String urlText = url.toString();
454 if(period.isHot()) urlText += "#bottom";
456 WebIPCDialog.showDialog(getTopFrame(), urlText);
462 * 個別の発言をWebブラウザで表示する。
464 private void actionShowWebTalk(){
465 TabBrowser browser = this.topView.getTabBrowser();
466 Village village = browser.getVillage();
467 if(village == null) return;
469 PeriodView periodView = currentPeriodView();
470 if(periodView == null) return;
472 Discussion discussion = periodView.getDiscussion();
473 Talk talk = discussion.getPopupedTalk();
474 if(talk == null) return;
476 Period period = periodView.getPeriod();
477 if(period == null) return;
479 Land land = village.getParentLand();
480 ServerAccess server = land.getServerAccess();
482 URL url = server.getPeriodURL(period);
484 String urlText = url.toString();
485 urlText += "#" + talk.getMessageID();
486 WebIPCDialog.showDialog(getTopFrame(), urlText);
492 * ポータルサイトをWebブラウザで表示する。
494 private void actionShowPortal(){
495 WebIPCDialog.showDialog(getTopFrame(), VerInfo.CONTACT);
500 * 例外発生による警告ダイアログへの反応を促す。
501 * @param title タイトル文字列
502 * @param message メッセージ
505 private void warnDialog(String title, String message, Throwable e){
506 LOGGER.log(Level.WARNING, message, e);
507 JOptionPane.showMessageDialog(
510 VerInfo.getFrameTitle(title),
511 JOptionPane.WARNING_MESSAGE );
518 private void actionChangeLaF(){
519 String className = this.actionManager.getSelectedLookAndFeel();
523 lnfClass = Class.forName(className);
524 }catch(ClassNotFoundException e){
525 String warnMsg = MessageFormat.format(
526 "このLook&Feel[{0}]を読み込む事ができません。",
528 warnDialog(ERRTITLE_LAF, warnMsg, e);
532 final LookAndFeel lnf;
534 lnf = (LookAndFeel) ( lnfClass.newInstance() );
535 }catch( InstantiationException
536 | IllegalAccessException
539 String warnMsg = MessageFormat.format(ERRFORM_LAF, className);
540 warnDialog(ERRTITLE_LAF, warnMsg, e);
544 Runnable lafTask = new Runnable(){
552 submitLightBusyTask(lafTask,
554 "Look&Feelが更新されました" );
560 * LookAndFeelの実際の更新を行う。
561 * @param lnf LookAndFeel
563 private void changeLaF(LookAndFeel lnf){
564 assert EventQueue.isDispatchThread();
567 UIManager.setLookAndFeel(lnf);
568 }catch(UnsupportedLookAndFeelException e){
569 String warnMsg = MessageFormat.format(
570 "このLook&Feel[{0}]はサポートされていません。",
572 warnDialog(ERRTITLE_LAF, warnMsg, e);
576 this.windowManager.changeAllWindowUI();
578 LOGGER.log(Level.INFO,
579 "Look&Feelが[{0}]に変更されました。", lnf.getName() );
587 private void actionShowFilter(){
588 FilterPanel filterPanel = this.windowManager.getFilterPanel();
589 toggleWindow(filterPanel);
596 private void actionShowAccount(){
597 AccountPanel accountPanel = this.windowManager.getAccountPanel();
598 toggleWindow(accountPanel);
605 private void actionShowLog(){
606 LogFrame logFrame = this.windowManager.getLogFrame();
607 toggleWindow(logFrame);
614 private void actionTalkPreview(){
615 TalkPreview talkPreview = this.windowManager.getTalkPreview();
616 toggleWindow(talkPreview);
623 private void actionOption(){
624 OptionPanel optionPanel = this.windowManager.getOptionPanel();
626 FontInfo fontInfo = this.appSetting.getFontInfo();
627 optionPanel.getFontChooser().setFontInfo(fontInfo);
629 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
630 optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
632 DialogPref dialogPref = this.appSetting.getDialogPref();
633 optionPanel.getDialogPrefPanel().setDialogPref(dialogPref);
635 optionPanel.setVisible(true);
636 if(optionPanel.isCanceled()) return;
638 fontInfo = optionPanel.getFontChooser().getFontInfo();
639 updateFontInfo(fontInfo);
641 proxyInfo = optionPanel.getProxyChooser().getProxyInfo();
642 updateProxyInfo(proxyInfo);
644 dialogPref = optionPanel.getDialogPrefPanel().getDialogPref();
645 updateDialogPref(dialogPref);
652 * @param newFontInfo 新フォント設定
654 private void updateFontInfo(final FontInfo newFontInfo){
655 FontInfo oldInfo = this.appSetting.getFontInfo();
657 if(newFontInfo.equals(oldInfo)) return;
658 this.appSetting.setFontInfo(newFontInfo);
660 this.topView.getTabBrowser().setFontInfo(newFontInfo);
662 TalkPreview talkPreview = this.windowManager.getTalkPreview();
663 OptionPanel optionPanel = this.windowManager.getOptionPanel();
664 FontChooser fontChooser = optionPanel.getFontChooser();
666 talkPreview.setFontInfo(newFontInfo);
667 fontChooser.setFontInfo(newFontInfo);
674 * @param newProxyInfo 新プロクシ設定
676 private void updateProxyInfo(ProxyInfo newProxyInfo){
677 ProxyInfo oldProxyInfo = this.appSetting.getProxyInfo();
679 if(newProxyInfo.equals(oldProxyInfo)) return;
680 this.appSetting.setProxyInfo(newProxyInfo);
682 for(Land land : this.model.getLandList()){
683 ServerAccess server = land.getServerAccess();
684 server.setProxy(newProxyInfo.getProxy());
692 * @param newDialogPref 表示設定
694 private void updateDialogPref(DialogPref newDialogPref){
695 DialogPref oldDialogPref = this.appSetting.getDialogPref();
697 if(newDialogPref.equals(oldDialogPref)) return;
698 this.appSetting.setDialogPref(newDialogPref);
700 this.topView.getTabBrowser().setDialogPref(newDialogPref);
708 private void actionShowDigest(){
709 TabBrowser browser = this.topView.getTabBrowser();
710 final Village village = browser.getVillage();
711 if(village == null) return;
713 VillageState villageState = village.getState();
714 if( ( villageState != VillageState.EPILOGUE
715 && villageState != VillageState.GAMEOVER
716 ) || ! village.isValid() ){
717 String message = "エピローグを正常に迎えていない村は\n"
719 String title = VerInfo.getFrameTitle("ダイジェスト不可");
720 JOptionPane pane = new JOptionPane(message,
721 JOptionPane.WARNING_MESSAGE,
722 JOptionPane.DEFAULT_OPTION );
723 JDialog dialog = pane.createDialog(getTopFrame(), title);
725 dialog.setVisible(true);
730 VillageDigest villageDigest = this.windowManager.getVillageDigest();
731 final VillageDigest digest = villageDigest;
732 Executor executor = Executors.newCachedThreadPool();
733 executor.execute(new Runnable(){
736 taskFullOpenAllPeriod();
737 EventQueue.invokeLater(new Runnable(){
740 digest.setVillage(village);
741 digest.setVisible(true);
753 * 全日程の一括フルオープン。ヘビータスク版。
755 // TODO taskLoadAllPeriodtと一体化したい。
756 private void taskFullOpenAllPeriod(){
758 updateStatusBar("一括読み込み開始");
760 TabBrowser browser = this.topView.getTabBrowser();
761 Village village = browser.getVillage();
762 if(village == null) return;
763 for(PeriodView periodView : browser.getPeriodViewList()){
764 Period period = periodView.getPeriod();
765 if(period == null) continue;
766 if(period.isFullOpen()) continue;
770 updateStatusBar(message);
772 Period.parsePeriod(period, true);
773 }catch(IOException e){
774 showNetworkError(village, e);
777 periodView.showTopics();
780 updateStatusBar("一括読み込み完了");
789 private void actionShowFind(){
790 FindPanel findPanel = this.windowManager.getFindPanel();
792 findPanel.setVisible(true);
793 if(findPanel.isCanceled()){
797 if(findPanel.isBulkSearch()){
808 private void regexSearch(){
809 Discussion discussion = currentDiscussion();
810 if(discussion == null) return;
812 FindPanel findPanel = this.windowManager.getFindPanel();
813 RegexPattern regPattern = findPanel.getRegexPattern();
814 int hits = discussion.setRegexPattern(regPattern);
816 String hitMessage = "[" + hits + "]件ヒットしました";
817 updateStatusBar(hitMessage);
820 if(regPattern != null){
821 Pattern pattern = regPattern.getPattern();
823 loginfo = "正規表現 " + pattern.pattern() + " に";
826 loginfo += hitMessage;
827 LOGGER.info(loginfo);
835 private void bulkSearch(){
836 Executor executor = Executors.newCachedThreadPool();
837 executor.execute(new Runnable(){
849 private void taskBulkSearch(){
852 FindPanel findPanel = this.windowManager.getFindPanel();
853 RegexPattern regPattern = findPanel.getRegexPattern();
854 StringBuilder hitDesc = new StringBuilder();
855 TabBrowser browser = this.topView.getTabBrowser();
856 for(PeriodView periodView : browser.getPeriodViewList()){
857 Discussion discussion = periodView.getDiscussion();
858 int hits = discussion.setRegexPattern(regPattern);
862 Period period = discussion.getPeriod();
863 hitDesc.append(' ').append(period.getDay()).append("d:");
864 hitDesc.append(hits).append("件");
868 "[" + totalhits + "]件ヒットしました。"
869 + hitDesc.toString();
870 updateStatusBar(hitMessage);
873 if(regPattern != null){
874 Pattern pattern = regPattern.getPattern();
876 loginfo = "正規表現 " + pattern.pattern() + " に";
879 loginfo += hitMessage;
880 LOGGER.info(loginfo);
886 * 検索パネルに現在選択中のPeriodを反映させる。
888 private void updateFindPanel(){
889 Discussion discussion = currentDiscussion();
890 if(discussion == null) return;
891 RegexPattern pattern = discussion.getRegexPattern();
892 FindPanel findPanel = this.windowManager.getFindPanel();
893 findPanel.setRegexPattern(pattern);
900 private void actionDaySummary(){
901 PeriodView periodView = currentPeriodView();
902 if(periodView == null) return;
904 Period period = periodView.getPeriod();
905 if(period == null) return;
907 DaySummary daySummary = this.windowManager.getDaySummary();
908 daySummary.summaryPeriod(period);
909 daySummary.setVisible(true);
915 * 表示中PeriodをCSVファイルへエクスポートする。
917 private void actionDayExportCsv(){
918 PeriodView periodView = currentPeriodView();
919 if(periodView == null) return;
921 Period period = periodView.getPeriod();
922 if(period == null) return;
924 FilterPanel filterPanel = this.windowManager.getFilterPanel();
925 File file = CsvExporter.exportPeriod(period, filterPanel);
927 String message = "CSVファイル("
930 updateStatusBar(message);
933 // TODO 長そうなジョブなら別スレッドにした方がいいか?
941 private void actionSearchNext(){
942 Discussion discussion = currentDiscussion();
943 if(discussion == null) return;
945 discussion.nextHotTarget();
953 private void actionSearchPrev(){
954 Discussion discussion = currentDiscussion();
955 if(discussion == null) return;
957 discussion.prevHotTarget();
965 private void actionReloadPeriod(){
968 TabBrowser tabBrowser = this.topView.getTabBrowser();
969 Village village = tabBrowser.getVillage();
970 if(village == null) return;
971 if(village.getState() != VillageState.EPILOGUE) return;
973 Discussion discussion = currentDiscussion();
974 if(discussion == null) return;
975 Period period = discussion.getPeriod();
976 if(period == null) return;
977 if(period.getTopics() > 1000){
978 JOptionPane.showMessageDialog(getTopFrame(),
979 "エピローグが1000発言を超えはじめたら、\n"
980 +"負荷対策のためWebブラウザによるアクセスを"
983 JOptionPane.WARNING_MESSAGE
993 private void actionLoadAllPeriod(){
994 Executor executor = Executors.newCachedThreadPool();
995 executor.execute(new Runnable(){
1007 * 全日程の一括ロード。ヘビータスク版。
1009 private void taskLoadAllPeriod(){
1011 updateStatusBar("一括読み込み開始");
1013 TabBrowser browser = this.topView.getTabBrowser();
1014 Village village = browser.getVillage();
1015 if(village == null) return;
1016 for(PeriodView periodView : browser.getPeriodViewList()){
1017 Period period = periodView.getPeriod();
1018 if(period == null) continue;
1021 + "日目のデータを読み込んでいます";
1022 updateStatusBar(message);
1024 Period.parsePeriod(period, false);
1025 }catch(IOException e){
1026 showNetworkError(village, e);
1029 periodView.showTopics();
1032 updateStatusBar("一括読み込み完了");
1041 private void actionReloadVillageList(){
1042 JTree tree = this.topView.getTreeView();
1043 TreePath path = tree.getSelectionPath();
1044 if(path == null) return;
1047 for(int ct = 0; ct < path.getPathCount(); ct++){
1048 Object obj = path.getPathComponent(ct);
1049 if(obj instanceof Land){
1054 if(land == null) return;
1056 this.topView.showInitPanel();
1058 submitReloadVillageList(land);
1064 * 選択文字列をクリップボードにコピーする。
1066 private void actionCopySelected(){
1067 Discussion discussion = currentDiscussion();
1068 if(discussion == null) return;
1070 CharSequence copied = discussion.copySelected();
1071 if(copied == null) return;
1073 copied = StringUtils.suppressString(copied);
1075 "[" + copied + "]をクリップボードにコピーしました");
1080 * 一発言のみクリップボードにコピーする。
1082 private void actionCopyTalk(){
1083 Discussion discussion = currentDiscussion();
1084 if(discussion == null) return;
1086 CharSequence copied = discussion.copyTalk();
1087 if(copied == null) return;
1089 copied = StringUtils.suppressString(copied);
1091 "[" + copied + "]をクリップボードにコピーしました");
1098 private void actionJumpAnchor(){
1099 PeriodView periodView = currentPeriodView();
1100 if(periodView == null) return;
1101 Discussion discussion = periodView.getDiscussion();
1103 final TabBrowser browser = this.topView.getTabBrowser();
1104 final Village village = browser.getVillage();
1105 final Anchor anchor = discussion.getPopupedAnchor();
1106 if(anchor == null) return;
1108 Executor executor = Executors.newCachedThreadPool();
1109 executor.execute(new Runnable(){
1113 updateStatusBar("ジャンプ先の読み込み中…");
1115 if(anchor.hasTalkNo()){
1117 taskLoadAllPeriod();
1120 final List<Talk> talkList;
1122 talkList = village.getTalkListFromAnchor(anchor);
1123 if(talkList == null || talkList.size() <= 0){
1131 final Talk targetTalk = talkList.get(0);
1132 final Period targetPeriod = targetTalk.getPeriod();
1133 final int tabIndex = targetPeriod.getDay() + 1;
1134 final PeriodView target = browser.getPeriodView(tabIndex);
1136 EventQueue.invokeLater(new Runnable(){
1139 browser.setSelectedIndex(tabIndex);
1140 target.setPeriod(targetPeriod);
1141 target.scrollToTalk(targetTalk);
1149 }catch(IOException e){
1151 "アンカーの展開中にエラーが起きました");
1164 * 指定した国の村一覧を読み込むジョブを投下。
1167 private void submitReloadVillageList(final Land land){
1168 Runnable heavyTask = new Runnable(){
1171 taskReloadVillageList(land);
1176 submitHeavyBusyTask(heavyTask,
1184 * 指定した国の村一覧を読み込む。(ヘビータスク本体).
1187 private void taskReloadVillageList(Land land){
1188 SortedSet<Village> villageList;
1190 villageList = land.downloadVillageList();
1191 }catch(IOException e){
1192 showNetworkError(land, e);
1195 land.updateVillageList(villageList);
1197 this.model.updateVillageList(land);
1199 LandsTree treePanel = this.topView.getLandsTree();
1200 treePanel.expandLand(land);
1207 * @param force trueならPeriodデータを強制再読み込み。
1209 private void updatePeriod(final boolean force){
1210 final TabBrowser tabBrowser = this.topView.getTabBrowser();
1211 final Village village = tabBrowser.getVillage();
1212 if(village == null) return;
1213 setFrameTitle(village.getVillageFullName());
1215 final PeriodView periodView = currentPeriodView();
1216 Discussion discussion = currentDiscussion();
1217 if(discussion == null) return;
1218 FilterPanel filterPanel = this.windowManager.getFilterPanel();
1219 discussion.setTopicFilter(filterPanel);
1220 final Period period = discussion.getPeriod();
1221 if(period == null) return;
1223 Executor executor = Executors.newCachedThreadPool();
1224 executor.execute(new Runnable(){
1229 boolean wasHot = loadPeriod();
1231 if(wasHot && ! period.isHot() ){
1232 if( ! updatePeriodList() ) return;
1242 private boolean loadPeriod(){
1243 updateStatusBar("1日分のデータを読み込んでいます…");
1246 wasHot = period.isHot();
1248 Period.parsePeriod(period, force);
1249 }catch(IOException e){
1250 showNetworkError(village, e);
1253 updateStatusBar("1日分のデータを読み終わりました");
1258 private boolean updatePeriodList(){
1259 updateStatusBar("村情報を読み直しています…");
1261 Village.updateVillage(village);
1262 }catch(IOException e){
1263 showNetworkError(village, e);
1267 SwingUtilities.invokeAndWait(new Runnable(){
1270 tabBrowser.setVillage(village);
1274 }catch(InvocationTargetException | InterruptedException e){
1275 LOGGER.log(Level.SEVERE,
1276 "タブ操作で致命的な障害が発生しました", e);
1278 updateStatusBar("村情報を読み直しました…");
1282 private void renderBrowser(){
1283 updateStatusBar("レンダリング中…");
1285 final int lastPos = periodView.getVerticalPosition();
1287 SwingUtilities.invokeAndWait(new Runnable(){
1290 periodView.showTopics();
1294 }catch( InvocationTargetException
1295 | InterruptedException
1297 LOGGER.log(Level.SEVERE,
1298 "ブラウザ表示で致命的な障害が発生しました",
1301 EventQueue.invokeLater(new Runnable(){
1304 periodView.setVerticalPosition(lastPos);
1308 updateStatusBar("レンダリング完了");
1320 private void filterChanged(){
1321 final Discussion discussion = currentDiscussion();
1322 if(discussion == null) return;
1324 FilterPanel filterPanel = this.windowManager.getFilterPanel();
1326 discussion.setTopicFilter(filterPanel);
1327 discussion.filtering();
1333 * 現在選択中のPeriodを内包するPeriodViewを返す。
1334 * @return PeriodView
1336 private PeriodView currentPeriodView(){
1337 TabBrowser tb = this.topView.getTabBrowser();
1338 PeriodView result = tb.currentPeriodView();
1343 * 現在選択中のPeriodを内包するDiscussionを返す。
1344 * @return Discussion
1346 private Discussion currentDiscussion(){
1347 PeriodView periodView = currentPeriodView();
1348 if(periodView == null) return null;
1349 Discussion result = periodView.getDiscussion();
1355 * @param window フレーム
1357 private void toggleWindow(Window window){
1358 if(window == null) return;
1360 if(window instanceof Frame){
1361 Frame frame = (Frame) window;
1362 int winState = frame.getExtendedState();
1363 boolean isIconified = (winState & Frame.ICONIFIED) != 0;
1365 winState &= ~(Frame.ICONIFIED);
1366 frame.setExtendedState(winState);
1367 frame.setVisible(true);
1372 if(window.isVisible()){
1373 window.setVisible(false);
1376 window.setVisible(true);
1382 * ネットワークエラーを通知するモーダルダイアログを表示する。
1383 * OKボタンを押すまでこのメソッドは戻ってこない。
1385 * @param e ネットワークエラー
1387 public void showNetworkError(Village village, IOException e){
1388 Land land = village.getParentLand();
1389 showNetworkError(land, e);
1394 * ネットワークエラーを通知するモーダルダイアログを表示する。
1395 * OKボタンを押すまでこのメソッドは戻ってこない。
1397 * @param e ネットワークエラー
1399 public void showNetworkError(Land land, IOException e){
1400 LOGGER.log(Level.WARNING, "ネットワークで障害が発生しました", e);
1402 ServerAccess server = land.getServerAccess();
1404 land.getLandDef().getLandName()
1406 +"何らかのトラブルが発生しました。\n"
1407 +"相手サーバのURLは [ " + server.getBaseURL() + " ] だよ。\n"
1409 +"Webブラウザでも遊べないか確認してみてね!\n";
1411 JOptionPane pane = new JOptionPane(message,
1412 JOptionPane.WARNING_MESSAGE,
1413 JOptionPane.DEFAULT_OPTION );
1415 String title = VerInfo.getFrameTitle("通信異常発生");
1416 JDialog dialog = pane.createDialog(getTopFrame(), title);
1419 dialog.setVisible(true);
1427 * ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
1428 * @param event イベント {@inheritDoc}
1431 public void valueChanged(TreeSelectionEvent event){
1432 TreePath path = event.getNewLeadSelectionPath();
1433 if(path == null) return;
1435 Object selObj = path.getLastPathComponent();
1437 if( selObj instanceof Land ){
1438 Land land = (Land) selObj;
1439 setFrameTitle(land.getLandDef().getLandName());
1440 this.topView.showLandInfo(land);
1441 this.actionManager.appearVillage(false);
1442 this.actionManager.appearPeriod(false);
1443 }else if( selObj instanceof Village ){
1444 final Village village = (Village) selObj;
1446 Executor executor = Executors.newCachedThreadPool();
1447 executor.execute(new Runnable(){
1451 updateStatusBar("村情報を読み込み中…");
1454 Village.updateVillage(village);
1455 }catch(IOException e){
1456 showNetworkError(village, e);
1459 updateStatusBar("村情報の読み込み完了");
1463 Controller.this.actionManager.appearVillage(true);
1464 setFrameTitle(village.getVillageFullName());
1466 EventQueue.invokeLater(new Runnable(){
1469 Controller.this.topView.showVillageInfo(village);
1484 * Periodがタブ選択されたときもしくは発言フィルタが操作されたときの処理。
1485 * @param event イベント {@inheritDoc}
1488 public void stateChanged(ChangeEvent event){
1489 Object source = event.getSource();
1491 if(source == this.windowManager.getFilterPanel()){
1493 }else if(source instanceof TabBrowser){
1495 updatePeriod(false);
1496 PeriodView periodView = currentPeriodView();
1497 if(periodView == null) this.actionManager.appearPeriod(false);
1498 else this.actionManager.appearPeriod(true);
1506 * @param e イベント {@inheritDoc}
1509 public void actionPerformed(ActionEvent e){
1510 if(this.isBusyNow) return;
1512 String cmd = e.getActionCommand();
1513 if(cmd.equals(ActionManager.CMD_ACCOUNT)){
1514 actionShowAccount();
1515 }else if(cmd.equals(ActionManager.CMD_EXIT)){
1517 }else if(cmd.equals(ActionManager.CMD_COPY)){
1518 actionCopySelected();
1519 }else if(cmd.equals(ActionManager.CMD_SHOWFIND)){
1521 }else if(cmd.equals(ActionManager.CMD_SEARCHNEXT)){
1523 }else if(cmd.equals(ActionManager.CMD_SEARCHPREV)){
1525 }else if(cmd.equals(ActionManager.CMD_ALLPERIOD)){
1526 actionLoadAllPeriod();
1527 }else if(cmd.equals(ActionManager.CMD_SHOWDIGEST)){
1529 }else if(cmd.equals(ActionManager.CMD_WEBVILL)){
1530 actionShowWebVillage();
1531 }else if(cmd.equals(ActionManager.CMD_WEBWIKI)){
1532 actionShowWebWiki();
1533 }else if(cmd.equals(ActionManager.CMD_RELOAD)){
1534 actionReloadPeriod();
1535 }else if(cmd.equals(ActionManager.CMD_DAYSUMMARY)){
1537 }else if(cmd.equals(ActionManager.CMD_DAYEXPCSV)){
1538 actionDayExportCsv();
1539 }else if(cmd.equals(ActionManager.CMD_WEBDAY)){
1541 }else if(cmd.equals(ActionManager.CMD_OPTION)){
1543 }else if(cmd.equals(ActionManager.CMD_LANDF)){
1545 }else if(cmd.equals(ActionManager.CMD_SHOWFILT)){
1547 }else if(cmd.equals(ActionManager.CMD_SHOWEDIT)){
1548 actionTalkPreview();
1549 }else if(cmd.equals(ActionManager.CMD_SHOWLOG)){
1551 }else if(cmd.equals(ActionManager.CMD_HELPDOC)){
1553 }else if(cmd.equals(ActionManager.CMD_SHOWPORTAL)){
1555 }else if(cmd.equals(ActionManager.CMD_ABOUT)){
1557 }else if(cmd.equals(ActionManager.CMD_VILLAGELIST)){
1558 actionReloadVillageList();
1559 }else if(cmd.equals(ActionManager.CMD_COPYTALK)){
1561 }else if(cmd.equals(ActionManager.CMD_JUMPANCHOR)){
1563 }else if(cmd.equals(ActionManager.CMD_WEBTALK)){
1564 actionShowWebTalk();
1571 * 村選択ツリーリストが畳まれるとき呼ばれる。
1572 * @param event ツリーイベント {@inheritDoc}
1575 public void treeWillCollapse(TreeExpansionEvent event){
1581 * 村選択ツリーリストが展開されるとき呼ばれる。
1582 * @param event ツリーイベント {@inheritDoc}
1585 public void treeWillExpand(TreeExpansionEvent event){
1586 if(!(event.getSource() instanceof JTree)){
1590 TreePath path = event.getPath();
1591 Object lastObj = path.getLastPathComponent();
1592 if(!(lastObj instanceof Land)){
1595 final Land land = (Land) lastObj;
1596 if(land.getVillageCount() > 0){
1600 submitReloadVillageList(land);
1607 * @param event {@inheritDoc}
1610 public void anchorHitted(AnchorHitEvent event){
1611 PeriodView periodView = currentPeriodView();
1612 if(periodView == null) return;
1613 Period period = periodView.getPeriod();
1614 if(period == null) return;
1615 final Village village = period.getVillage();
1617 final TalkDraw talkDraw = event.getTalkDraw();
1618 final Anchor anchor = event.getAnchor();
1619 final Discussion discussion = periodView.getDiscussion();
1621 Executor executor = Executors.newCachedThreadPool();
1622 executor.execute(new Runnable(){
1626 updateStatusBar("アンカーの展開中…");
1628 if(anchor.hasTalkNo()){
1630 taskLoadAllPeriod();
1633 final List<Talk> talkList;
1635 talkList = village.getTalkListFromAnchor(anchor);
1636 if(talkList == null || talkList.size() <= 0){
1643 EventQueue.invokeLater(new Runnable(){
1646 talkDraw.showAnchorTalks(anchor, talkList);
1647 discussion.layoutRows();
1655 }catch(IOException e){
1657 "アンカーの展開中にエラーが起きました");
1671 * プログレスバーとカーソルの設定を行う。
1672 * @param isBusy trueならプログレスバーのアニメ開始&WAITカーソル。
1675 private void setBusy(final boolean isBusy){
1676 this.isBusyNow = isBusy;
1678 Runnable microJob = new Runnable(){
1683 cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
1685 cursor = Cursor.getDefaultCursor();
1688 Component glass = getTopFrame().getGlassPane();
1689 glass.setCursor(cursor);
1690 glass.setVisible(isBusy);
1691 Controller.this.topView.setBusy(isBusy);
1697 if(SwingUtilities.isEventDispatchThread()){
1701 SwingUtilities.invokeAndWait(microJob);
1702 }catch(InvocationTargetException | InterruptedException e){
1703 LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
1712 * @param message メッセージ
1714 private void updateStatusBar(String message){
1715 this.topView.updateSysMessage(message);
1719 * トップフレームのタイトルを設定する。
1720 * タイトルは指定された国or村名 + " - Jindolf"
1723 private void setFrameTitle(String name){
1724 String title = VerInfo.getFrameTitle(name);
1725 TopFrame topFrame = this.windowManager.getTopFrame();
1726 topFrame.setTitle(title);
1733 private void shutdown(){
1734 ConfigStore configStore = this.appSetting.getConfigStore();
1736 FindPanel findPanel = this.windowManager.getFindPanel();
1737 JsObject findConf = findPanel.getJson();
1738 if( ! findPanel.hasConfChanged(findConf) ){
1739 configStore.saveHistoryConfig(findConf);
1742 TalkPreview talkPreview = this.windowManager.getTalkPreview();
1743 JsObject draftConf = talkPreview.getJson();
1744 if( ! talkPreview.hasConfChanged(draftConf) ){
1745 configStore.saveDraftConfig(draftConf);
1748 this.appSetting.saveConfig();
1750 LOGGER.info("VMごとアプリケーションを終了します。");
1751 System.exit(0); // invoke shutdown hooks... BYE !