4 * License : The MIT License
5 * Copyright(c) 2008 olyutorskii
8 package jp.sfjp.jindolf;
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;
18 import java.io.IOException;
19 import java.lang.reflect.InvocationTargetException;
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;
95 * いわゆるMVCでいうとこのコントローラ。
97 public class Controller
98 implements ActionListener,
100 private static final Logger LOGGER = Logger.getAnonymousLogger();
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}]を生成する事ができません。";
109 private final LandsTreeModel model;
110 private final WindowManager windowManager;
111 private final ActionManager actionManager;
112 private final AppSetting appSetting;
114 private final TopView topView;
116 private final VillageTreeWatcher treeVillageWatcher =
117 new VillageTreeWatcher();
118 private final ChangeListener tabPeriodWatcher =
119 new TabPeriodWatcher();
120 private final ChangeListener filterWatcher =
123 private final Executor executor = Executors.newCachedThreadPool();
124 private volatile boolean isBusyNow;
129 * @param model 最上位データモデル
130 * @param windowManager ウィンドウ管理
131 * @param actionManager アクション管理
132 * @param setting アプリ設定
134 @SuppressWarnings("LeakingThisInConstructor")
135 public Controller(LandsTreeModel model,
136 WindowManager windowManager,
137 ActionManager actionManager,
141 this.appSetting = setting;
142 this.actionManager = actionManager;
143 this.windowManager = windowManager;
146 this.topView = this.windowManager.getTopFrame().getTopView();
148 JToolBar toolbar = this.actionManager.getBrowseToolBar();
149 this.topView.setBrowseToolBar(toolbar);
151 this.actionManager.addActionListener(this);
153 JTree treeView = this.topView.getTreeView();
154 treeView.setModel(this.model);
155 treeView.addTreeWillExpandListener(this.treeVillageWatcher);
156 treeView.addTreeSelectionListener(this.treeVillageWatcher);
158 TabBrowser periodTab = this.topView.getTabBrowser();
159 periodTab.addChangeListener(this.tabPeriodWatcher);
160 periodTab.addActionListener(this);
161 periodTab.addAnchorHitListener(this);
163 JButton reloadVillageListButton = this.topView
165 .getReloadVillageListButton();
166 reloadVillageListButton.addActionListener(this);
167 reloadVillageListButton.setEnabled(false);
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();
177 topFrame.setJMenuBar(this.actionManager.getMenuBar());
179 topFrame.setDefaultCloseOperation(
180 WindowConstants.DISPOSE_ON_CLOSE);
181 topFrame.addWindowListener(new WindowAdapter(){
184 public void windowClosed(WindowEvent event){
189 filterPanel.addChangeListener(this.filterWatcher);
191 Handler newHandler = logFrame.getHandler();
192 LogUtils.switchHandler(newHandler);
194 ConfigStore config = this.appSetting.getConfigStore();
196 JsObject draft = config.loadDraftConfig();
197 talkPreview.putJson(draft);
199 JsObject history = config.loadHistoryConfig();
200 findPanel.putJson(history);
202 FontInfo fontInfo = this.appSetting.getFontInfo();
203 periodTab.setFontInfo(fontInfo);
204 talkPreview.setFontInfo(fontInfo);
205 optionPanel.getFontChooser().setFontInfo(fontInfo);
207 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
208 optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
210 DialogPref pref = this.appSetting.getDialogPref();
211 periodTab.setDialogPref(pref);
212 optionPanel.getDialogPrefPanel().setDialogPref(pref);
214 OptionInfo optInfo = this.appSetting.getOptionInfo();
215 ConfigStore configStore = this.appSetting.getConfigStore();
216 helpFrame.updateVmInfo(optInfo, configStore);
226 private static void toggleWindow(Window window){
227 if(window == null) return;
229 if(window instanceof Frame){
230 Frame frame = (Frame) window;
231 int winState = frame.getExtendedState();
232 boolean isIconified = (winState & Frame.ICONIFIED) != 0;
234 winState &= ~(Frame.ICONIFIED);
235 frame.setExtendedState(winState);
236 frame.setVisible(true);
241 if(window.isVisible()){
242 window.setVisible(false);
245 window.setVisible(true);
255 public WindowManager getWindowManager(){
256 return this.windowManager;
263 public TopFrame getTopFrame(){
264 TopFrame result = this.windowManager.getTopFrame();
270 * タイトルは指定された国or村名 + " - Jindolf"
273 private void setFrameTitle(String name){
274 String title = VerInfo.getFrameTitle(name);
275 TopFrame topFrame = this.windowManager.getTopFrame();
276 topFrame.setTitle(title);
281 * 現在選択中のPeriodを内包するPeriodViewを返す。
284 private PeriodView currentPeriodView(){
285 TabBrowser tb = this.topView.getTabBrowser();
286 PeriodView result = tb.currentPeriodView();
291 * 現在選択中のPeriodを内包するDiscussionを返す。
294 private Discussion currentDiscussion(){
295 PeriodView periodView = currentPeriodView();
296 if(periodView == null) return null;
297 Discussion result = periodView.getDiscussion();
304 * @return 選択中の村。なければnull。
306 private Village getVillage(){
307 TabBrowser browser = this.topView.getTabBrowser();
308 Village village = browser.getVillage();
315 * <p>ヘビーなタスク実行をアピールするために、
316 * プログレスバーとカーソルの設定を行う。
318 * <p>ビジー中のActionコマンド受信は無視される。
320 * <p>ビジー中のトップフレームのマウス操作、キーボード入力は
323 * @param isBusy trueならプログレスバーのアニメ開始&WAITカーソル。
324 * falseなら停止&通常カーソル。
325 * @param msg フッタメッセージ。nullなら変更なし。
327 private void setBusy(boolean isBusy, String msg){
328 this.isBusyNow = isBusy;
330 TopFrame topFrame = getTopFrame();
332 topFrame.setBusy(isBusy);
334 this.topView.updateSysMessage(msg);
342 * @param message メッセージ
344 private void updateStatusBar(String message){
345 this.topView.updateSysMessage(message);
352 * <p>EDT以外から呼ばれると実際の処理が次回のEDT移行に遅延される。
354 * @param isBusy ビジーならtrue
355 * @param message ステータスバー表示。nullなら変更なし
357 public void submitBusyStatus(boolean isBusy, String message){
358 Runnable task = () -> {
359 setBusy(isBusy, message);
362 if(EventQueue.isDispatchThread()){
366 EventQueue.invokeAndWait(task);
367 }catch(InvocationTargetException | InterruptedException e){
368 LOGGER.log(Level.SEVERE, "ビジー処理で失敗", e);
378 * <p>タスク実行中はビジー状態となる。
380 * <p>軽量タスク実行中はイベントループが停止するので、
381 * 入出力待ちを伴わなずに早急に終わるタスクでなければならない。
384 * @param beforeMsg ビジー中ステータス文字列
385 * @param afterMsg ビジー復帰時のステータス文字列
387 public void submitLightBusyTask(Runnable task,
390 submitBusyStatus(true, beforeMsg);
391 EventQueue.invokeLater(task);
392 submitBusyStatus(false, afterMsg);
398 * 重量級タスクをEDTとは別のスレッドで実行する。
400 * <p>タスク実行中はビジー状態となる。
402 * @param heavyTask 重量級タスク
403 * @param beforeMsg ビジー中ステータス文字列
404 * @param afterMsg ビジー復帰時のステータス文字列
406 public void submitHeavyBusyTask(final Runnable heavyTask,
407 final String beforeMsg,
408 final String afterMsg ){
409 submitBusyStatus(true, beforeMsg);
411 EventQueue.invokeLater(() -> {
416 submitBusyStatus(false, afterMsg);
425 * スレッドプールを用いて非EDTなタスクを投入する。
429 private void fork(Runnable task){
430 this.executor.execute(task);
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());
444 JDialog dialog = pane.createDialog(getTopFrame(),
445 VerInfo.TITLE + "について");
448 dialog.setVisible(true);
457 private void actionExit(){
465 private void actionHelp(){
466 HelpFrame helpFrame = this.windowManager.getHelpFrame();
467 toggleWindow(helpFrame);
474 private void actionShowWebVillage(){
475 Village village = getVillage();
476 if(village == null) return;
478 Land land = village.getParentLand();
479 ServerAccess server = land.getServerAccess();
481 URL url = server.getVillageURL(village);
483 String urlText = url.toString();
484 if(village.getState() != VillageState.GAMEOVER){
485 urlText += "#bottom";
488 WebIPCDialog.showDialog(getTopFrame(), urlText);
494 * 村に対応するまとめサイトをWebブラウザで表示する。
496 private void actionShowWebWiki(){
497 Village village = getVillage();
498 if(village == null) return;
500 String urlTxt = WolfBBS.getCastGeneratorUrl(village);
501 WebIPCDialog.showDialog(getTopFrame(), urlTxt);
507 * 日(Period)をWebブラウザで表示する。
509 private void actionShowWebDay(){
510 PeriodView periodView = currentPeriodView();
511 if(periodView == null) return;
513 Period period = periodView.getPeriod();
514 if(period == null) return;
516 Village village = getVillage();
517 if(village == null) return;
519 Land land = village.getParentLand();
520 ServerAccess server = land.getServerAccess();
522 URL url = server.getPeriodURL(period);
524 String urlText = url.toString();
526 WebIPCDialog.showDialog(getTopFrame(), urlText);
532 * 個別の発言をWebブラウザで表示する。
534 private void actionShowWebTalk(){
535 Village village = getVillage();
536 if(village == null) return;
538 PeriodView periodView = currentPeriodView();
539 if(periodView == null) return;
541 Discussion discussion = periodView.getDiscussion();
542 Talk talk = discussion.getActiveTalk();
543 if(talk == null) return;
545 Period period = periodView.getPeriod();
546 if(period == null) return;
548 Land land = village.getParentLand();
549 ServerAccess server = land.getServerAccess();
551 URL url = server.getPeriodURL(period);
553 String urlText = url.toString();
554 urlText += "#" + talk.getMessageID();
555 WebIPCDialog.showDialog(getTopFrame(), urlText);
561 * ポータルサイトをWebブラウザで表示する。
563 private void actionShowPortal(){
564 WebIPCDialog.showDialog(getTopFrame(), VerInfo.CONTACT);
569 * 例外発生による警告ダイアログへの反応を促す。
570 * @param title タイトル文字列
571 * @param message メッセージ
574 private void warnDialog(String title, String message, Throwable e){
575 LOGGER.log(Level.WARNING, message, e);
576 JOptionPane.showMessageDialog(
579 VerInfo.getFrameTitle(title),
580 JOptionPane.WARNING_MESSAGE );
587 private void actionChangeLaF(){
588 String className = this.actionManager.getSelectedLookAndFeel();
589 if(className == null) return;
592 () -> {taskChangeLaF(className);},
601 * LookAndFeelの実際の更新を行う軽量タスク。
603 * @param lnf LookAndFeel
605 private void taskChangeLaF(String className){
606 assert EventQueue.isDispatchThread();
609 this.windowManager.changeAllWindowUI(className);
610 }catch(UnsupportedLookAndFeelException e){
611 String warnMsg = MessageFormat.format(
612 "このLook&Feel[{0}]はサポートされていません。",
614 warnDialog(ERRTITLE_LAF, warnMsg, e);
616 }catch(ReflectiveOperationException e){
617 String warnMsg = MessageFormat.format(ERRFORM_LAFGEN, className);
618 warnDialog(ERRTITLE_LAF, warnMsg, e);
622 LOGGER.log(Level.INFO,
623 "Look&Feelが[{0}]に変更されました。", className );
631 private void actionShowFilter(){
632 FilterPanel filterPanel = this.windowManager.getFilterPanel();
633 toggleWindow(filterPanel);
640 private void actionShowLog(){
641 LogFrame logFrame = this.windowManager.getLogFrame();
642 toggleWindow(logFrame);
649 private void actionTalkPreview(){
650 TalkPreview talkPreview = this.windowManager.getTalkPreview();
651 toggleWindow(talkPreview);
658 private void actionOption(){
659 OptionPanel optionPanel = this.windowManager.getOptionPanel();
661 FontInfo fontInfo = this.appSetting.getFontInfo();
662 optionPanel.getFontChooser().setFontInfo(fontInfo);
664 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
665 optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
667 DialogPref dialogPref = this.appSetting.getDialogPref();
668 optionPanel.getDialogPrefPanel().setDialogPref(dialogPref);
670 optionPanel.setVisible(true);
671 if(optionPanel.isCanceled()) return;
673 fontInfo = optionPanel.getFontChooser().getFontInfo();
674 updateFontInfo(fontInfo);
676 proxyInfo = optionPanel.getProxyChooser().getProxyInfo();
677 updateProxyInfo(proxyInfo);
679 dialogPref = optionPanel.getDialogPrefPanel().getDialogPref();
680 updateDialogPref(dialogPref);
687 * @param newFontInfo 新フォント設定
689 private void updateFontInfo(final FontInfo newFontInfo){
690 FontInfo oldInfo = this.appSetting.getFontInfo();
692 if(newFontInfo.equals(oldInfo)) return;
693 this.appSetting.setFontInfo(newFontInfo);
695 this.topView.getTabBrowser().setFontInfo(newFontInfo);
697 TalkPreview talkPreview = this.windowManager.getTalkPreview();
698 OptionPanel optionPanel = this.windowManager.getOptionPanel();
699 FontChooser fontChooser = optionPanel.getFontChooser();
701 talkPreview.setFontInfo(newFontInfo);
702 fontChooser.setFontInfo(newFontInfo);
709 * @param newProxyInfo 新プロクシ設定
711 private void updateProxyInfo(ProxyInfo newProxyInfo){
712 ProxyInfo oldProxyInfo = this.appSetting.getProxyInfo();
714 if(newProxyInfo.equals(oldProxyInfo)) return;
715 this.appSetting.setProxyInfo(newProxyInfo);
717 for(Land land : this.model.getLandList()){
718 ServerAccess server = land.getServerAccess();
719 server.setProxy(newProxyInfo.getProxy());
727 * @param newDialogPref 表示設定
729 private void updateDialogPref(DialogPref newDialogPref){
730 DialogPref oldDialogPref = this.appSetting.getDialogPref();
732 if(newDialogPref.equals(oldDialogPref)) return;
733 this.appSetting.setDialogPref(newDialogPref);
735 this.topView.getTabBrowser().setDialogPref(newDialogPref);
743 private void actionShowDigest(){
744 Village village = getVillage();
745 if(village == null) return;
747 VillageState villageState = village.getState();
748 if( ( villageState != VillageState.EPILOGUE
749 && villageState != VillageState.GAMEOVER
750 ) || ! village.isValid() ){
751 String message = "エピローグを正常に迎えていない村は\n"
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);
759 dialog.setVisible(true);
764 VillageDigest villageDigest = this.windowManager.getVillageDigest();
765 final VillageDigest digest = villageDigest;
767 Runnable task = () -> {
768 taskFullOpenAllPeriod();
769 EventQueue.invokeLater(() -> {
770 digest.setVillage(village);
771 digest.setVisible(true);
785 * 全日程の一括フルオープン。ヘビータスク版。
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;
798 updateStatusBar(message);
800 PeriodLoader.parsePeriod(period, false);
801 }catch(IOException e){
802 showNetworkError(village, e);
805 periodView.showTopics();
814 private void actionShowFind(){
815 FindPanel findPanel = this.windowManager.getFindPanel();
817 findPanel.setVisible(true);
818 if(findPanel.isCanceled()){
822 if(findPanel.isBulkSearch()){
833 private void regexSearch(){
834 Discussion discussion = currentDiscussion();
835 if(discussion == null) return;
837 FindPanel findPanel = this.windowManager.getFindPanel();
838 RegexPattern regPattern = findPanel.getRegexPattern();
839 int hits = discussion.setRegexPattern(regPattern);
841 String hitMessage = "[" + hits + "]件ヒットしました";
842 updateStatusBar(hitMessage);
845 if(regPattern != null){
846 Pattern pattern = regPattern.getPattern();
848 loginfo = "正規表現 " + pattern.pattern() + " に";
851 loginfo += hitMessage;
852 LOGGER.info(loginfo);
860 private void bulkSearch(){
862 () -> {taskBulkSearch();},
871 private void taskBulkSearch(){
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);
884 Period period = discussion.getPeriod();
885 hitDesc.append(' ').append(period.getDay()).append("d:");
886 hitDesc.append(hits).append("件");
890 "[" + totalhits + "]件ヒットしました。"
891 + hitDesc.toString();
892 updateStatusBar(hitMessage);
895 if(regPattern != null){
896 Pattern pattern = regPattern.getPattern();
898 loginfo = "正規表現 " + pattern.pattern() + " に";
901 loginfo += hitMessage;
902 LOGGER.info(loginfo);
908 * 検索パネルに現在選択中のPeriodを反映させる。
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);
922 private void actionDaySummary(){
923 PeriodView periodView = currentPeriodView();
924 if(periodView == null) return;
926 Period period = periodView.getPeriod();
927 if(period == null) return;
929 DaySummary daySummary = this.windowManager.getDaySummary();
930 daySummary.summaryPeriod(period);
931 daySummary.setVisible(true);
937 * 表示中PeriodをCSVファイルへエクスポートする。
939 private void actionDayExportCsv(){
940 PeriodView periodView = currentPeriodView();
941 if(periodView == null) return;
943 Period period = periodView.getPeriod();
944 if(period == null) return;
946 FilterPanel filterPanel = this.windowManager.getFilterPanel();
947 File file = CsvExporter.exportPeriod(period, filterPanel);
949 String message = "CSVファイル("
952 updateStatusBar(message);
955 // TODO 長そうなジョブなら別スレッドにした方がいいか?
963 private void actionSearchNext(){
964 Discussion discussion = currentDiscussion();
965 if(discussion == null) return;
967 discussion.nextHotTarget();
975 private void actionSearchPrev(){
976 Discussion discussion = currentDiscussion();
977 if(discussion == null) return;
979 discussion.prevHotTarget();
987 private void actionReloadPeriod(){
990 Village village = getVillage();
991 if(village == null) return;
992 if(village.getState() != VillageState.EPILOGUE) return;
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ブラウザによるアクセスを"
1004 JOptionPane.WARNING_MESSAGE
1014 private void actionLoadAllPeriod(){
1015 submitHeavyBusyTask(
1016 () -> {taskLoadAllPeriod();},
1025 * 全日程の一括ロード。ヘビータスク版。
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;
1036 + "日目のデータを読み込んでいます";
1037 updateStatusBar(message);
1039 PeriodLoader.parsePeriod(period, false);
1040 }catch(IOException e){
1041 showNetworkError(village, e);
1044 periodView.showTopics();
1053 private void actionReloadVillageList(){
1054 JTree tree = this.topView.getTreeView();
1055 TreePath path = tree.getSelectionPath();
1056 if(path == null) return;
1059 for(int ct = 0; ct < path.getPathCount(); ct++){
1060 Object obj = path.getPathComponent(ct);
1061 if(obj instanceof Land){
1066 if(land == null) return;
1068 this.topView.showInitPanel();
1070 submitReloadVillageList(land);
1076 * 選択文字列をクリップボードにコピーする。
1078 private void actionCopySelected(){
1079 Discussion discussion = currentDiscussion();
1080 if(discussion == null) return;
1082 CharSequence copied = discussion.copySelected();
1083 if(copied == null) return;
1085 copied = StringUtils.suppressString(copied);
1087 "[" + copied + "]をクリップボードにコピーしました");
1092 * 一発言のみクリップボードにコピーする。
1094 private void actionCopyTalk(){
1095 Discussion discussion = currentDiscussion();
1096 if(discussion == null) return;
1098 CharSequence copied = discussion.copyTalk();
1099 if(copied == null) return;
1101 copied = StringUtils.suppressString(copied);
1103 "[" + copied + "]をクリップボードにコピーしました");
1108 * アンカー先を含むPeriodの全会話を事前にロードする。
1111 * @param anchor アンカー
1112 * @return アンカー先を含むPeriod。
1113 * アンカーがG国発言番号ならnull。
1114 * Periodが見つからないならnull。
1115 * @throws IOException 入力エラー
1117 private Period loadAnchoredPeriod(Village village, Anchor anchor)
1119 if(anchor.hasTalkNo()) return null;
1121 Period anchorPeriod = village.getPeriod(anchor);
1122 if(anchorPeriod == null) return null;
1124 PeriodLoader.parsePeriod(anchorPeriod, false);
1126 return anchorPeriod;
1132 private void actionJumpAnchor(){
1133 PeriodView periodView = currentPeriodView();
1134 if(periodView == null) return;
1135 Discussion discussion = periodView.getDiscussion();
1137 TabBrowser browser = this.topView.getTabBrowser();
1138 Village village = getVillage();
1139 final Anchor anchor = discussion.getActiveAnchor();
1140 if(anchor == null) return;
1142 Runnable task = () -> {
1143 if(anchor.hasTalkNo()){
1145 taskLoadAllPeriod();
1148 final List<Talk> talkList;
1150 loadAnchoredPeriod(village, anchor);
1151 talkList = village.getTalkListFromAnchor(anchor);
1152 if(talkList == null || talkList.size() <= 0){
1160 Talk targetTalk = talkList.get(0);
1161 Period targetPeriod = targetTalk.getPeriod();
1162 int periodIndex = targetPeriod.getDay();
1163 PeriodView target = browser.getPeriodView(periodIndex);
1165 EventQueue.invokeLater(() -> {
1166 browser.showPeriodTab(periodIndex);
1167 target.setPeriod(targetPeriod);
1168 target.scrollToTalk(targetTalk);
1174 }catch(IOException e){
1176 "アンカーの展開中にエラーが起きました");
1181 submitHeavyBusyTask(
1191 * ローカルなXMLファイルを読み込む。
1193 private void actionOpenXml(){
1194 JFileChooser chooser = new JFileChooser();
1195 chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
1198 filter = new FileNameExtensionFilter("XML files (*.xml)", "xml", "XML");
1199 chooser.setFileFilter(filter);
1201 int result = chooser.showOpenDialog(getTopFrame());
1202 if(result != JFileChooser.APPROVE_OPTION) return;
1203 File selected = chooser.getSelectedFile();
1205 submitHeavyBusyTask(() -> {
1208 village = VillageLoader.parseVillage(selected);
1209 }catch(IOException e){
1210 System.out.println(e);
1213 village.setLocalArchive(true);
1214 AvatarPics avatarPics = village.getAvatarPics();
1215 this.appSetting.applyLocalImage(avatarPics);
1216 avatarPics.preload();
1217 EventQueue.invokeLater(() -> {
1218 selectedVillage(village);
1220 }, "XML読み込み中", "XML読み込み完了");
1226 * 指定した国の村一覧を読み込むジョブを投下。
1229 private void submitReloadVillageList(final Land land){
1230 submitHeavyBusyTask(
1231 () -> {taskReloadVillageList(land);},
1239 * 指定した国の村一覧を読み込む。(ヘビータスク本体).
1242 private void taskReloadVillageList(Land land){
1243 List<Village> villageList;
1245 villageList = VillageListLoader.loadVillageList(land);
1246 }catch(IOException e){
1247 showNetworkError(land, e);
1250 land.updateVillageList(villageList);
1252 this.model.updateVillageList(land);
1254 LandsTree treePanel = this.topView.getLandsTree();
1255 treePanel.expandLand(land);
1262 * @param force trueならPeriodデータを強制再読み込み。
1264 private void updatePeriod(final boolean force){
1265 Village village = getVillage();
1266 if(village == null) return;
1268 String fullName = village.getVillageFullName();
1269 setFrameTitle(fullName);
1271 PeriodView periodView = currentPeriodView();
1272 Discussion discussion = currentDiscussion();
1273 if(discussion == null) return;
1275 FilterPanel filterPanel = this.windowManager.getFilterPanel();
1276 discussion.setTopicFilter(filterPanel);
1278 Period period = discussion.getPeriod();
1279 if(period == null) return;
1281 Runnable task = () -> {
1283 PeriodLoader.parsePeriod(period, force);
1284 }catch(IOException e){
1285 showNetworkError(village, e);
1289 EventQueue.invokeLater(() -> {
1290 int lastPos = periodView.getVerticalPosition();
1291 periodView.showTopics();
1292 periodView.setVerticalPosition(lastPos);
1296 submitHeavyBusyTask(
1308 private void filterChanged(){
1309 final Discussion discussion = currentDiscussion();
1310 if(discussion == null) return;
1312 FilterPanel filterPanel = this.windowManager.getFilterPanel();
1314 discussion.setTopicFilter(filterPanel);
1315 discussion.filtering();
1321 * ネットワークエラーを通知するモーダルダイアログを表示する。
1322 * OKボタンを押すまでこのメソッドは戻ってこない。
1324 * @param e ネットワークエラー
1326 public void showNetworkError(Village village, IOException e){
1327 Land land = village.getParentLand();
1328 showNetworkError(land, e);
1333 * ネットワークエラーを通知するモーダルダイアログを表示する。
1334 * OKボタンを押すまでこのメソッドは戻ってこない。
1336 * @param e ネットワークエラー
1338 public void showNetworkError(Land land, IOException e){
1339 LOGGER.log(Level.WARNING, "ネットワークで障害が発生しました", e);
1341 ServerAccess server = land.getServerAccess();
1343 land.getLandDef().getLandName()
1345 +"何らかのトラブルが発生しました。\n"
1346 +"相手サーバのURLは [ " + server.getBaseURL() + " ] だよ。\n"
1348 +"Webブラウザでも遊べないか確認してみてね!\n";
1350 JOptionPane pane = new JOptionPane(message,
1351 JOptionPane.WARNING_MESSAGE,
1352 JOptionPane.DEFAULT_OPTION );
1354 String title = VerInfo.getFrameTitle("通信異常発生");
1355 JDialog dialog = pane.createDialog(getTopFrame(), title);
1358 dialog.setVisible(true);
1369 private void selectedLand(Land land){
1370 String landName = land.getLandDef().getLandName();
1371 setFrameTitle(landName);
1373 this.actionManager.exposeVillage(false);
1374 this.actionManager.exposePeriod(false);
1376 this.topView.showLandInfo(land);
1386 private void selectedVillage(Village village){
1387 setFrameTitle(village.getVillageFullName());
1388 if(village.isLocalArchive()){
1389 this.actionManager.exposeVillageLocal(true);
1391 this.actionManager.exposeVillage(true);
1394 Runnable task = () -> {
1396 if( ! village.hasSchedule() ){
1397 VillageInfoLoader.updateVillageInfo(village);
1399 }catch(IOException e){
1400 showNetworkError(village, e);
1404 EventQueue.invokeLater(() -> {
1405 this.topView.showVillageInfo(village);
1409 submitHeavyBusyTask(
1421 * <p>主にメニュー選択やボタン押下などのアクションをディスパッチする。
1425 * @param ev {@inheritDoc}
1428 public void actionPerformed(ActionEvent ev){
1429 if(this.isBusyNow) return;
1431 String cmd = ev.getActionCommand();
1432 if(cmd == null) return;
1435 case ActionManager.CMD_OPENXML:
1438 case ActionManager.CMD_EXIT:
1441 case ActionManager.CMD_COPY:
1442 actionCopySelected();
1444 case ActionManager.CMD_SHOWFIND:
1447 case ActionManager.CMD_SEARCHNEXT:
1450 case ActionManager.CMD_SEARCHPREV:
1453 case ActionManager.CMD_ALLPERIOD:
1454 actionLoadAllPeriod();
1456 case ActionManager.CMD_SHOWDIGEST:
1459 case ActionManager.CMD_WEBVILL:
1460 actionShowWebVillage();
1462 case ActionManager.CMD_WEBWIKI:
1463 actionShowWebWiki();
1465 case ActionManager.CMD_RELOAD:
1466 actionReloadPeriod();
1468 case ActionManager.CMD_DAYSUMMARY:
1471 case ActionManager.CMD_DAYEXPCSV:
1472 actionDayExportCsv();
1474 case ActionManager.CMD_WEBDAY:
1477 case ActionManager.CMD_OPTION:
1480 case ActionManager.CMD_LANDF:
1483 case ActionManager.CMD_SHOWFILT:
1486 case ActionManager.CMD_SHOWEDIT:
1487 actionTalkPreview();
1489 case ActionManager.CMD_SHOWLOG:
1492 case ActionManager.CMD_HELPDOC:
1495 case ActionManager.CMD_SHOWPORTAL:
1498 case ActionManager.CMD_ABOUT:
1501 case ActionManager.CMD_VILLAGELIST:
1502 actionReloadVillageList();
1504 case ActionManager.CMD_COPYTALK:
1507 case ActionManager.CMD_JUMPANCHOR:
1510 case ActionManager.CMD_WEBTALK:
1511 actionShowWebTalk();
1522 * @param event {@inheritDoc}
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();
1532 final TalkDraw talkDraw = event.getTalkDraw();
1533 final Anchor anchor = event.getAnchor();
1534 final Discussion discussion = periodView.getDiscussion();
1536 Runnable task = () -> {
1537 if(anchor.hasTalkNo()){
1539 taskLoadAllPeriod();
1542 final List<Talk> talkList;
1544 loadAnchoredPeriod(village, anchor);
1545 talkList = village.getTalkListFromAnchor(anchor);
1546 if(talkList == null || talkList.size() <= 0){
1553 EventQueue.invokeLater(() -> {
1554 talkDraw.showAnchorTalks(anchor, talkList);
1555 discussion.layoutRows();
1561 }catch(IOException e){
1563 "アンカーの展開中にエラーが起きました");
1567 submitHeavyBusyTask(
1579 private void shutdown(){
1580 ConfigStore configStore = this.appSetting.getConfigStore();
1582 FindPanel findPanel = this.windowManager.getFindPanel();
1583 JsObject findConf = findPanel.getJson();
1584 if( ! findPanel.hasConfChanged(findConf) ){
1585 configStore.saveHistoryConfig(findConf);
1588 TalkPreview talkPreview = this.windowManager.getTalkPreview();
1589 JsObject draftConf = talkPreview.getJson();
1590 if( ! talkPreview.hasConfChanged(draftConf) ){
1591 configStore.saveDraftConfig(draftConf);
1594 this.appSetting.saveConfig();
1596 LOGGER.info("VMごとアプリケーションを終了します。");
1597 System.exit(0); // invoke shutdown hooks... BYE !
1607 private class FilterWatcher implements ChangeListener{
1621 * <p>発言フィルタが操作されたときの処理。
1623 * @param event {@inheritDoc}
1626 public void stateChanged(ChangeEvent event){
1627 Object source = event.getSource();
1629 if(source == Controller.this.windowManager.getFilterPanel()){
1639 * Period一覧タブのタブ操作を監視する。
1641 private class TabPeriodWatcher implements ChangeListener{
1655 * <p>Periodがタブ選択されたときの処理。
1657 * @param event {@inheritDoc}
1660 public void stateChanged(ChangeEvent event){
1661 Object source = event.getSource();
1663 if(source instanceof TabBrowser){
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);
1676 Controller.this.actionManager.exposeVillage(hasCurrentPeriod);
1687 * 国村選択リストの選択展開操作を監視する。
1689 private class VillageTreeWatcher
1690 implements TreeSelectionListener, TreeWillExpandListener{
1695 VillageTreeWatcher(){
1704 * <p>ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
1706 * @param event {@inheritDoc}
1709 public void valueChanged(TreeSelectionEvent event){
1710 TreePath path = event.getNewLeadSelectionPath();
1711 if(path == null) return;
1713 Object selObj = path.getLastPathComponent();
1714 if(selObj instanceof Land){
1715 Land land = (Land) selObj;
1717 }else if(selObj instanceof Village){
1718 Village village = (Village) selObj;
1719 village.setLocalArchive(false);
1720 selectedVillage(village);
1729 * <p>村選択ツリーリストが畳まれるとき呼ばれる。
1731 * @param event ツリーイベント {@inheritDoc}
1734 public void treeWillCollapse(TreeExpansionEvent event){
1741 * <p>村選択ツリーリストが展開されるとき呼ばれる。
1743 * @param event ツリーイベント {@inheritDoc}
1746 public void treeWillExpand(TreeExpansionEvent event){
1747 if(!(event.getSource() instanceof JTree)){
1751 TreePath path = event.getPath();
1752 Object lastObj = path.getLastPathComponent();
1753 if(!(lastObj instanceof Land)){
1756 Land land = (Land) lastObj;
1757 if(land.getVillageCount() > 0){
1761 submitReloadVillageList(land);