4 * License : The MIT License
5 * Copyright(c) 2008 olyutorskii
8 package jp.sfjp.jindolf;
10 import java.awt.BorderLayout;
11 import java.awt.Component;
12 import java.awt.Container;
13 import java.awt.Cursor;
14 import java.awt.EventQueue;
15 import java.awt.Frame;
16 import java.awt.LayoutManager;
17 import java.awt.Window;
18 import java.awt.event.ActionEvent;
19 import java.awt.event.ActionListener;
20 import java.awt.event.KeyAdapter;
21 import java.awt.event.MouseAdapter;
22 import java.awt.event.WindowAdapter;
23 import java.awt.event.WindowEvent;
25 import java.io.IOException;
26 import java.io.UnsupportedEncodingException;
27 import java.lang.reflect.InvocationTargetException;
29 import java.net.URLEncoder;
30 import java.util.HashMap;
31 import java.util.List;
34 import java.util.concurrent.Executor;
35 import java.util.concurrent.Executors;
36 import java.util.logging.Handler;
37 import java.util.logging.Logger;
38 import java.util.regex.Pattern;
39 import javax.swing.JButton;
40 import javax.swing.JComponent;
41 import javax.swing.JDialog;
42 import javax.swing.JFrame;
43 import javax.swing.JOptionPane;
44 import javax.swing.JToolBar;
45 import javax.swing.JTree;
46 import javax.swing.LookAndFeel;
47 import javax.swing.SwingUtilities;
48 import javax.swing.UIManager;
49 import javax.swing.UnsupportedLookAndFeelException;
50 import javax.swing.WindowConstants;
51 import javax.swing.event.ChangeEvent;
52 import javax.swing.event.ChangeListener;
53 import javax.swing.event.TreeExpansionEvent;
54 import javax.swing.event.TreeSelectionEvent;
55 import javax.swing.event.TreeSelectionListener;
56 import javax.swing.event.TreeWillExpandListener;
57 import javax.swing.tree.TreePath;
58 import jp.sfjp.jindolf.config.AppSetting;
59 import jp.sfjp.jindolf.config.ConfigStore;
60 import jp.sfjp.jindolf.config.OptionInfo;
61 import jp.sfjp.jindolf.data.Anchor;
62 import jp.sfjp.jindolf.data.DialogPref;
63 import jp.sfjp.jindolf.data.Land;
64 import jp.sfjp.jindolf.data.LandsModel;
65 import jp.sfjp.jindolf.data.Period;
66 import jp.sfjp.jindolf.data.RegexPattern;
67 import jp.sfjp.jindolf.data.Talk;
68 import jp.sfjp.jindolf.data.Village;
69 import jp.sfjp.jindolf.dxchg.CsvExporter;
70 import jp.sfjp.jindolf.dxchg.WebIPCDialog;
71 import jp.sfjp.jindolf.editor.TalkPreview;
72 import jp.sfjp.jindolf.glyph.AnchorHitEvent;
73 import jp.sfjp.jindolf.glyph.AnchorHitListener;
74 import jp.sfjp.jindolf.glyph.Discussion;
75 import jp.sfjp.jindolf.glyph.FontInfo;
76 import jp.sfjp.jindolf.glyph.TalkDraw;
77 import jp.sfjp.jindolf.log.LogFrame;
78 import jp.sfjp.jindolf.log.LogUtils;
79 import jp.sfjp.jindolf.log.LogWrapper;
80 import jp.sfjp.jindolf.net.ProxyInfo;
81 import jp.sfjp.jindolf.net.ServerAccess;
82 import jp.sfjp.jindolf.summary.DaySummary;
83 import jp.sfjp.jindolf.summary.VillageDigest;
84 import jp.sfjp.jindolf.util.GUIUtils;
85 import jp.sfjp.jindolf.util.StringUtils;
86 import jp.sfjp.jindolf.view.AccountPanel;
87 import jp.sfjp.jindolf.view.ActionManager;
88 import jp.sfjp.jindolf.view.FilterPanel;
89 import jp.sfjp.jindolf.view.FindPanel;
90 import jp.sfjp.jindolf.view.HelpFrame;
91 import jp.sfjp.jindolf.view.LandsTree;
92 import jp.sfjp.jindolf.view.OptionPanel;
93 import jp.sfjp.jindolf.view.PeriodView;
94 import jp.sfjp.jindolf.view.TabBrowser;
95 import jp.sfjp.jindolf.view.TopView;
96 import jp.sourceforge.jindolf.corelib.LandDef;
97 import jp.sourceforge.jindolf.corelib.VillageState;
98 import jp.sourceforge.jovsonz.JsObject;
101 * いわゆるMVCでいうとこのコントローラ。
103 public class Controller
104 implements ActionListener,
105 TreeWillExpandListener,
106 TreeSelectionListener,
110 private static final String TITLE_LOGGER =
111 VerInfo.getFrameTitle("ログ表示");
112 private static final String TITLE_FILTER =
113 VerInfo.getFrameTitle("発言フィルタ");
114 private static final String TITLE_EDITOR =
115 VerInfo.getFrameTitle("発言エディタ");
116 private static final String TITLE_OPTION =
117 VerInfo.getFrameTitle("オプション設定");
118 private static final String TITLE_FIND =
119 VerInfo.getFrameTitle("発言検索");
120 private static final String TITLE_ACCOUNT =
121 VerInfo.getFrameTitle("アカウント管理");
122 private static final String TITLE_DIGEST =
123 VerInfo.getFrameTitle("村のダイジェスト");
124 private static final String TITLE_DAYSUMMARY =
125 VerInfo.getFrameTitle("発言集計");
126 private static final String TITLE_HELP =
127 VerInfo.getFrameTitle("ヘルプ");
129 private static final LogWrapper LOGGER = new LogWrapper();
132 private final AppSetting appSetting;
133 private final ActionManager actionManager;
134 private final TopView topView;
135 private final LandsModel model;
137 private final FilterPanel filterFrame;
138 private final LogFrame showlogFrame;
139 private final OptionPanel optionPanel;
140 private final FindPanel findPanel;
141 private final TalkPreview talkPreview;
142 private JFrame helpFrame;
143 private AccountPanel accountFrame;
144 private DaySummary daySummaryPanel;
145 private VillageDigest digestPanel;
146 private final Map<Window, Boolean> windowMap =
147 new HashMap<Window, Boolean>();
149 private volatile boolean isBusyNow;
151 private JFrame topFrame = null;
155 * @param setting アプリ設定
156 * @param actionManager アクション管理
157 * @param topView 最上位ビュー
158 * @param model 最上位データモデル
160 @SuppressWarnings("LeakingThisInConstructor")
161 public Controller(AppSetting setting,
162 ActionManager actionManager,
167 this.appSetting = setting;
168 this.actionManager = actionManager;
169 this.topView = topView;
172 JToolBar toolbar = this.actionManager.getBrowseToolBar();
173 this.topView.setBrowseToolBar(toolbar);
175 this.actionManager.addActionListener(this);
177 JTree treeView = this.topView.getTreeView();
178 treeView.setModel(this.model);
179 treeView.addTreeWillExpandListener(this);
180 treeView.addTreeSelectionListener(this);
182 this.topView.getTabBrowser().addChangeListener(this);
183 this.topView.getTabBrowser().addActionListener(this);
184 this.topView.getTabBrowser().addAnchorHitListener(this);
186 JButton reloadVillageListButton = this.topView
188 .getReloadVillageListButton();
189 reloadVillageListButton.addActionListener(this);
190 reloadVillageListButton.setEnabled(false);
192 this.filterFrame = new FilterPanel(this.topFrame);
193 this.filterFrame.setTitle(TITLE_FILTER);
194 this.filterFrame.addChangeListener(this);
195 this.filterFrame.pack();
196 this.filterFrame.setVisible(false);
198 this.showlogFrame = new LogFrame(this.topFrame);
199 this.showlogFrame.setTitle(TITLE_LOGGER);
200 this.showlogFrame.pack();
201 this.showlogFrame.setSize(600, 500);
202 this.showlogFrame.setLocationByPlatform(true);
203 this.showlogFrame.setVisible(false);
204 Logger rootLogger = Logger.getLogger("");
205 Handler newHandler = this.showlogFrame.getHandler();
206 LogUtils.switchHandler(rootLogger, newHandler);
208 this.talkPreview = new TalkPreview();
209 this.talkPreview.setTitle(TITLE_EDITOR);
210 this.talkPreview.pack();
211 this.talkPreview.setSize(700, 500);
212 this.talkPreview.setVisible(false);
214 this.optionPanel = new OptionPanel(this.topFrame);
215 this.optionPanel.setTitle(TITLE_OPTION);
216 this.optionPanel.pack();
217 this.optionPanel.setSize(450, 500);
218 this.optionPanel.setVisible(false);
220 this.findPanel = new FindPanel(this.topFrame);
221 this.findPanel.setTitle(TITLE_FIND);
222 this.findPanel.pack();
223 this.findPanel.setVisible(false);
225 this.windowMap.put(this.filterFrame, true);
226 this.windowMap.put(this.showlogFrame, false);
227 this.windowMap.put(this.talkPreview, false);
228 this.windowMap.put(this.optionPanel, false);
229 this.windowMap.put(this.findPanel, true);
231 ConfigStore config = this.appSetting.getConfigStore();
233 JsObject draft = config.loadDraftConfig();
234 this.talkPreview.putJson(draft);
236 JsObject history = config.loadHistoryConfig();
237 this.findPanel.putJson(history);
239 FontInfo fontInfo = this.appSetting.getFontInfo();
240 this.topView.getTabBrowser().setFontInfo(fontInfo);
241 this.talkPreview.setFontInfo(fontInfo);
242 this.optionPanel.getFontChooser().setFontInfo(fontInfo);
244 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
245 this.optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
247 DialogPref pref = this.appSetting.getDialogPref();
248 this.topView.getTabBrowser().setDialogPref(pref);
249 this.optionPanel.getDialogPrefPanel().setDialogPref(pref);
258 @SuppressWarnings("serial")
259 public JFrame createTopFrame(){
260 this.topFrame = new JFrame();
262 Container content = this.topFrame.getContentPane();
263 LayoutManager layout = new BorderLayout();
264 content.setLayout(layout);
265 content.add(this.topView, BorderLayout.CENTER);
267 Component glassPane = new JComponent() {};
268 glassPane.addMouseListener(new MouseAdapter() {});
269 glassPane.addKeyListener(new KeyAdapter() {});
270 this.topFrame.setGlassPane(glassPane);
272 this.topFrame.setJMenuBar(this.actionManager.getMenuBar());
275 this.windowMap.put(this.topFrame, false);
277 this.topFrame.setDefaultCloseOperation(
278 WindowConstants.DISPOSE_ON_CLOSE);
279 this.topFrame.addWindowListener(new WindowAdapter(){
281 public void windowClosed(WindowEvent event){
286 return this.topFrame;
292 private void actionAbout(){
293 String message = VerInfo.getAboutMessage();
294 JOptionPane pane = new JOptionPane(message,
295 JOptionPane.INFORMATION_MESSAGE,
296 JOptionPane.DEFAULT_OPTION,
297 GUIUtils.getLogoIcon());
299 JDialog dialog = pane.createDialog(this.topFrame,
300 VerInfo.TITLE + "について");
303 dialog.setVisible(true);
312 private void actionExit(){
320 private void actionHelp(){
321 if(this.helpFrame != null){ // show Toggle
322 toggleWindow(this.helpFrame);
326 OptionInfo optInfo = this.appSetting.getOptionInfo();
327 ConfigStore configStore = this.appSetting.getConfigStore();
329 this.helpFrame = new HelpFrame(optInfo, configStore);
330 this.helpFrame.setTitle(TITLE_HELP);
331 this.helpFrame.pack();
332 this.helpFrame.setSize(450, 450);
334 this.windowMap.put(this.helpFrame, false);
336 this.helpFrame.setVisible(true);
344 private void actionShowWebVillage(){
345 TabBrowser browser = this.topView.getTabBrowser();
346 Village village = browser.getVillage();
347 if(village == null) return;
349 Land land = village.getParentLand();
350 ServerAccess server = land.getServerAccess();
352 URL url = server.getVillageURL(village);
354 String urlText = url.toString();
355 if(village.getState() != VillageState.GAMEOVER){
356 urlText += "#bottom";
359 WebIPCDialog.showDialog(this.topFrame, urlText);
365 * 村に対応するまとめサイトをWebブラウザで表示する。
367 private void actionShowWebWiki(){
368 TabBrowser browser = this.topView.getTabBrowser();
369 Village village = browser.getVillage();
370 if(village == null) return;
373 LandDef landDef = village.getParentLand().getLandDef();
374 if(landDef.getLandId().equals("wolfg")){
375 String vnum = "000" + village.getVillageID();
376 vnum = vnum.substring(vnum.length() - 3);
377 villageName = landDef.getLandPrefix() + vnum;
379 villageName = village.getVillageName();
384 .append("http://wolfbbs.jp/")
386 .append("%C2%BC.html");
388 WebIPCDialog.showDialog(this.topFrame, url.toString());
394 * 村に対応するキャスト紹介表ジェネレーターをWebブラウザで表示する。
396 private void actionShowWebCast(){
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 villageUrl = server.getVillageURL(village);
406 StringBuilder url = new StringBuilder("http://hon5.com/jinro/");
410 .append(URLEncoder.encode(villageUrl.toString(), "UTF-8"));
411 }catch(UnsupportedEncodingException e){
417 WebIPCDialog.showDialog(this.topFrame, url.toString());
423 * 日(Period)をWebブラウザで表示する。
425 private void actionShowWebDay(){
426 PeriodView periodView = currentPeriodView();
427 if(periodView == null) return;
429 Period period = periodView.getPeriod();
430 if(period == null) return;
432 TabBrowser browser = this.topView.getTabBrowser();
433 Village village = browser.getVillage();
434 if(village == null) return;
436 Land land = village.getParentLand();
437 ServerAccess server = land.getServerAccess();
439 URL url = server.getPeriodURL(period);
441 String urlText = url.toString();
442 if(period.isHot()) urlText += "#bottom";
444 WebIPCDialog.showDialog(this.topFrame, urlText);
450 * 個別の発言をWebブラウザで表示する。
452 private void actionShowWebTalk(){
453 TabBrowser browser = this.topView.getTabBrowser();
454 Village village = browser.getVillage();
455 if(village == null) return;
457 PeriodView periodView = currentPeriodView();
458 if(periodView == null) return;
460 Discussion discussion = periodView.getDiscussion();
461 Talk talk = discussion.getPopupedTalk();
462 if(talk == null) return;
464 Period period = periodView.getPeriod();
465 if(period == null) return;
467 Land land = village.getParentLand();
468 ServerAccess server = land.getServerAccess();
470 URL url = server.getPeriodURL(period);
472 String urlText = url.toString();
473 urlText += "#" + talk.getMessageID();
474 WebIPCDialog.showDialog(this.topFrame, urlText);
480 * ポータルサイトをWebブラウザで表示する。
482 private void actionShowPortal(){
483 WebIPCDialog.showDialog(this.topFrame, VerInfo.CONTACT);
488 * 例外発生による警告ダイアログへの反応を促す。
489 * @param title タイトル文字列
490 * @param message メッセージ
493 private void warnDialog(String title, String message, Throwable e){
494 LOGGER.warn(message, e);
495 JOptionPane.showMessageDialog(
498 VerInfo.getFrameTitle(title),
499 JOptionPane.WARNING_MESSAGE );
506 private void actionChangeLaF(){
507 String className = this.actionManager.getSelectedLookAndFeel();
509 String warnTitle = "Look&Feel";
513 warnMsg = "このLook&Feel[" + className + "]を読み込む事ができません。";
515 lnfClass = Class.forName(className);
516 }catch(ClassNotFoundException e){
517 warnDialog(warnTitle, warnMsg, e);
522 warnMsg = "このLook&Feel[" + className + "]を生成する事ができません。";
524 lnf = (LookAndFeel)( lnfClass.newInstance() );
525 }catch(InstantiationException e){
526 warnDialog(warnTitle, warnMsg, e);
528 }catch(IllegalAccessException e){
529 warnDialog(warnTitle, warnMsg, e);
531 }catch(ClassCastException e){
532 warnDialog(warnTitle, warnMsg, e);
536 warnMsg = "このLook&Feel[" + lnf.getName() + "]はサポートされていません。";
538 UIManager.setLookAndFeel(lnf);
539 }catch(UnsupportedLookAndFeelException e){
540 warnDialog(warnTitle, warnMsg, e);
549 final Runnable updateUITask = new Runnable(){
551 Set<Window> windows = Controller.this.windowMap.keySet();
552 for(Window window : windows){
553 SwingUtilities.updateComponentTreeUI(window);
555 boolean needPack = Controller.this.windowMap.get(window);
565 Executor executor = Executors.newCachedThreadPool();
566 executor.execute(new Runnable(){
569 updateStatusBar("Look&Feelを更新中…");
571 SwingUtilities.invokeAndWait(updateUITask);
572 }catch(InvocationTargetException e){
574 "Look&Feelの更新に失敗しました。", e);
575 }catch(InterruptedException e){
577 "Look&Feelの更新に失敗しました。", e);
579 updateStatusBar("Look&Feelが更新されました");
592 private void actionShowFilter(){
593 toggleWindow(this.filterFrame);
600 private void actionShowAccount(){
601 if(this.accountFrame != null){ // show Toggle
602 toggleWindow(this.accountFrame);
606 this.accountFrame = new AccountPanel(this.topFrame, this.model);
607 this.accountFrame.setTitle(TITLE_ACCOUNT);
608 this.accountFrame.pack();
609 this.accountFrame.setVisible(true);
611 this.windowMap.put(this.accountFrame, true);
619 private void actionShowLog(){
620 toggleWindow(this.showlogFrame);
627 private void actionTalkPreview(){
628 toggleWindow(this.talkPreview);
635 private void actionOption(){
636 FontInfo fontInfo = this.appSetting.getFontInfo();
637 this.optionPanel.getFontChooser().setFontInfo(fontInfo);
639 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
640 this.optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
642 DialogPref dialogPref = this.appSetting.getDialogPref();
643 this.optionPanel.getDialogPrefPanel().setDialogPref(dialogPref);
645 this.optionPanel.setVisible(true);
646 if(this.optionPanel.isCanceled()) return;
648 fontInfo = this.optionPanel.getFontChooser().getFontInfo();
649 updateFontInfo(fontInfo);
651 proxyInfo = this.optionPanel.getProxyChooser().getProxyInfo();
652 updateProxyInfo(proxyInfo);
654 dialogPref = this.optionPanel.getDialogPrefPanel().getDialogPref();
655 updateDialogPref(dialogPref);
662 * @param newFontInfo 新フォント設定
664 private void updateFontInfo(final FontInfo newFontInfo){
665 FontInfo oldInfo = this.appSetting.getFontInfo();
667 if(newFontInfo.equals(oldInfo)) return;
668 this.appSetting.setFontInfo(newFontInfo);
670 this.topView.getTabBrowser().setFontInfo(newFontInfo);
671 this.talkPreview.setFontInfo(newFontInfo);
672 this.optionPanel.getFontChooser().setFontInfo(newFontInfo);
679 * @param newProxyInfo 新プロクシ設定
681 private void updateProxyInfo(ProxyInfo newProxyInfo){
682 ProxyInfo oldProxyInfo = this.appSetting.getProxyInfo();
684 if(newProxyInfo.equals(oldProxyInfo)) return;
685 this.appSetting.setProxyInfo(newProxyInfo);
687 for(Land land : this.model.getLandList()){
688 ServerAccess server = land.getServerAccess();
689 server.setProxy(newProxyInfo.getProxy());
697 * @param newDialogPref 表示設定
699 private void updateDialogPref(DialogPref newDialogPref){
700 DialogPref oldDialogPref = this.appSetting.getDialogPref();
702 if(newDialogPref.equals(oldDialogPref)) return;
703 this.appSetting.setDialogPref(newDialogPref);
705 this.topView.getTabBrowser().setDialogPref(newDialogPref);
713 private void actionShowDigest(){
714 TabBrowser browser = this.topView.getTabBrowser();
715 final Village village = browser.getVillage();
716 if(village == null) return;
718 VillageState villageState = village.getState();
719 if(( villageState != VillageState.EPILOGUE
720 && villageState != VillageState.GAMEOVER
721 ) || ! village.isValid() ){
722 String message = "エピローグを正常に迎えていない村は\n"
724 String title = VerInfo.getFrameTitle("ダイジェスト不可");
725 JOptionPane pane = new JOptionPane(message,
726 JOptionPane.WARNING_MESSAGE,
727 JOptionPane.DEFAULT_OPTION );
728 JDialog dialog = pane.createDialog(this.topFrame, title);
730 dialog.setVisible(true);
735 if(this.digestPanel == null){
736 this.digestPanel = new VillageDigest(this.topFrame);
737 this.digestPanel.setTitle(TITLE_DIGEST);
738 this.digestPanel.pack();
739 this.digestPanel.setSize(600, 550);
740 this.windowMap.put(this.digestPanel, false);
743 final VillageDigest digest = this.digestPanel;
744 Executor executor = Executors.newCachedThreadPool();
745 executor.execute(new Runnable(){
747 taskFullOpenAllPeriod();
748 EventQueue.invokeLater(new Runnable(){
750 digest.setVillage(village);
751 digest.setVisible(true);
763 * 全日程の一括フルオープン。ヘビータスク版。
765 // TODO taskLoadAllPeriodtと一体化したい。
766 private void taskFullOpenAllPeriod(){
768 updateStatusBar("一括読み込み開始");
770 TabBrowser browser = this.topView.getTabBrowser();
771 Village village = browser.getVillage();
772 if(village == null) return;
773 for(PeriodView periodView : browser.getPeriodViewList()){
774 Period period = periodView.getPeriod();
775 if(period == null) continue;
776 if(period.isFullOpen()) continue;
780 updateStatusBar(message);
782 Period.parsePeriod(period, true);
783 }catch(IOException e){
784 showNetworkError(village, e);
787 periodView.showTopics();
790 updateStatusBar("一括読み込み完了");
799 private void actionShowFind(){
800 this.findPanel.setVisible(true);
801 if(this.findPanel.isCanceled()){
805 if(this.findPanel.isBulkSearch()){
816 private void regexSearch(){
817 Discussion discussion = currentDiscussion();
818 if(discussion == null) return;
820 RegexPattern regPattern = this.findPanel.getRegexPattern();
821 int hits = discussion.setRegexPattern(regPattern);
823 String hitMessage = "[" + hits + "]件ヒットしました";
824 updateStatusBar(hitMessage);
827 if(regPattern != null){
828 Pattern pattern = regPattern.getPattern();
830 loginfo = "正規表現 " + pattern.pattern() + " に";
833 loginfo += hitMessage;
834 LOGGER.info(loginfo);
842 private void bulkSearch(){
843 Executor executor = Executors.newCachedThreadPool();
844 executor.execute(new Runnable(){
855 private void taskBulkSearch(){
858 RegexPattern regPattern = this.findPanel.getRegexPattern();
859 StringBuilder hitDesc = new StringBuilder();
860 TabBrowser browser = this.topView.getTabBrowser();
861 for(PeriodView periodView : browser.getPeriodViewList()){
862 Discussion discussion = periodView.getDiscussion();
863 int hits = discussion.setRegexPattern(regPattern);
867 Period period = discussion.getPeriod();
868 hitDesc.append(' ').append(period.getDay()).append("d:");
869 hitDesc.append(hits).append("件");
873 "[" + totalhits + "]件ヒットしました。"
874 + hitDesc.toString();
875 updateStatusBar(hitMessage);
878 if(regPattern != null){
879 Pattern pattern = regPattern.getPattern();
881 loginfo = "正規表現 " + pattern.pattern() + " に";
884 loginfo += hitMessage;
885 LOGGER.info(loginfo);
891 * 検索パネルに現在選択中のPeriodを反映させる。
893 private void updateFindPanel(){
894 Discussion discussion = currentDiscussion();
895 if(discussion == null) return;
896 RegexPattern pattern = discussion.getRegexPattern();
897 this.findPanel.setRegexPattern(pattern);
904 private void actionDaySummary(){
905 PeriodView periodView = currentPeriodView();
906 if(periodView == null) return;
908 Period period = periodView.getPeriod();
909 if(period == null) return;
911 if(this.daySummaryPanel == null){
912 this.daySummaryPanel = new DaySummary(this.topFrame);
913 this.daySummaryPanel.setTitle(TITLE_DAYSUMMARY);
914 this.daySummaryPanel.pack();
915 this.daySummaryPanel.setSize(400, 500);
918 this.daySummaryPanel.summaryPeriod(period);
919 this.daySummaryPanel.setVisible(true);
921 this.windowMap.put(this.daySummaryPanel, false);
927 * 表示中PeriodをCSVファイルへエクスポートする。
929 private void actionDayExportCsv(){
930 PeriodView periodView = currentPeriodView();
931 if(periodView == null) return;
933 Period period = periodView.getPeriod();
934 if(period == null) return;
936 File file = CsvExporter.exportPeriod(period, this.filterFrame);
938 String message = "CSVファイル("
941 updateStatusBar(message);
944 // TODO 長そうなジョブなら別スレッドにした方がいいか?
952 private void actionSearchNext(){
953 Discussion discussion = currentDiscussion();
954 if(discussion == null) return;
956 discussion.nextHotTarget();
964 private void actionSearchPrev(){
965 Discussion discussion = currentDiscussion();
966 if(discussion == null) return;
968 discussion.prevHotTarget();
976 private void actionReloadPeriod(){
979 TabBrowser tabBrowser = this.topView.getTabBrowser();
980 Village village = tabBrowser.getVillage();
981 if(village == null) return;
982 if(village.getState() != VillageState.EPILOGUE) return;
984 Discussion discussion = currentDiscussion();
985 if(discussion == null) return;
986 Period period = discussion.getPeriod();
987 if(period == null) return;
988 if(period.getTopics() > 1000){
989 JOptionPane.showMessageDialog(this.topFrame,
990 "エピローグが1000発言を超えはじめたら、\n"
991 +"負荷対策のためWebブラウザによるアクセスを"
994 JOptionPane.WARNING_MESSAGE
1004 private void actionLoadAllPeriod(){
1005 Executor executor = Executors.newCachedThreadPool();
1006 executor.execute(new Runnable(){
1008 taskLoadAllPeriod();
1017 * 全日程の一括ロード。ヘビータスク版。
1019 private void taskLoadAllPeriod(){
1021 updateStatusBar("一括読み込み開始");
1023 TabBrowser browser = this.topView.getTabBrowser();
1024 Village village = browser.getVillage();
1025 if(village == null) return;
1026 for(PeriodView periodView : browser.getPeriodViewList()){
1027 Period period = periodView.getPeriod();
1028 if(period == null) continue;
1031 + "日目のデータを読み込んでいます";
1032 updateStatusBar(message);
1034 Period.parsePeriod(period, false);
1035 }catch(IOException e){
1036 showNetworkError(village, e);
1039 periodView.showTopics();
1042 updateStatusBar("一括読み込み完了");
1051 private void actionReloadVillageList(){
1052 JTree tree = this.topView.getTreeView();
1053 TreePath path = tree.getSelectionPath();
1054 if(path == null) return;
1057 for(int ct = 0; ct < path.getPathCount(); ct++){
1058 Object obj = path.getPathComponent(ct);
1059 if(obj instanceof Land){
1064 if(land == null) return;
1066 this.topView.showInitPanel();
1068 execReloadVillageList(land);
1074 * 選択文字列をクリップボードにコピーする。
1076 private void actionCopySelected(){
1077 Discussion discussion = currentDiscussion();
1078 if(discussion == null) return;
1080 CharSequence copied = discussion.copySelected();
1081 if(copied == null) return;
1083 copied = StringUtils.suppressString(copied);
1085 "[" + copied + "]をクリップボードにコピーしました");
1090 * 一発言のみクリップボードにコピーする。
1092 private void actionCopyTalk(){
1093 Discussion discussion = currentDiscussion();
1094 if(discussion == null) return;
1096 CharSequence copied = discussion.copyTalk();
1097 if(copied == null) return;
1099 copied = StringUtils.suppressString(copied);
1101 "[" + copied + "]をクリップボードにコピーしました");
1108 private void actionJumpAnchor(){
1109 PeriodView periodView = currentPeriodView();
1110 if(periodView == null) return;
1111 Discussion discussion = periodView.getDiscussion();
1113 final TabBrowser browser = this.topView.getTabBrowser();
1114 final Village village = browser.getVillage();
1115 final Anchor anchor = discussion.getPopupedAnchor();
1116 if(anchor == null) return;
1118 Executor executor = Executors.newCachedThreadPool();
1119 executor.execute(new Runnable(){
1122 updateStatusBar("ジャンプ先の読み込み中…");
1124 if(anchor.hasTalkNo()){
1126 taskLoadAllPeriod();
1129 final List<Talk> talkList;
1131 talkList = village.getTalkListFromAnchor(anchor);
1132 if(talkList == null || talkList.size() <= 0){
1140 final Talk targetTalk = talkList.get(0);
1141 final Period targetPeriod = targetTalk.getPeriod();
1142 final int tabIndex = targetPeriod.getDay() + 1;
1143 final PeriodView target = browser.getPeriodView(tabIndex);
1145 EventQueue.invokeLater(new Runnable(){
1147 browser.setSelectedIndex(tabIndex);
1148 target.setPeriod(targetPeriod);
1149 target.scrollToTalk(targetTalk);
1157 }catch(IOException e){
1159 "アンカーの展開中にエラーが起きました");
1175 private void execReloadVillageList(final Land land){
1176 final LandsTree treePanel = this.topView.getLandsTree();
1177 Executor executor = Executors.newCachedThreadPool();
1178 executor.execute(new Runnable(){
1181 updateStatusBar("村一覧を読み込み中…");
1184 Controller.this.model.loadVillageList(land);
1185 }catch(IOException e){
1186 showNetworkError(land, e);
1188 treePanel.expandLand(land);
1190 updateStatusBar("村一覧の読み込み完了");
1201 * @param force trueならPeriodデータを強制再読み込み。
1203 private void updatePeriod(final boolean force){
1204 final TabBrowser tabBrowser = this.topView.getTabBrowser();
1205 final Village village = tabBrowser.getVillage();
1206 if(village == null) return;
1207 setFrameTitle(village.getVillageFullName());
1209 final PeriodView periodView = currentPeriodView();
1210 Discussion discussion = currentDiscussion();
1211 if(discussion == null) return;
1212 discussion.setTopicFilter(this.filterFrame);
1213 final Period period = discussion.getPeriod();
1214 if(period == null) return;
1216 Executor executor = Executors.newCachedThreadPool();
1217 executor.execute(new Runnable(){
1221 boolean wasHot = loadPeriod();
1223 if(wasHot && ! period.isHot() ){
1224 if( ! updatePeriodList() ) return;
1234 private boolean loadPeriod(){
1235 updateStatusBar("1日分のデータを読み込んでいます…");
1238 wasHot = period.isHot();
1240 Period.parsePeriod(period, force);
1241 }catch(IOException e){
1242 showNetworkError(village, e);
1245 updateStatusBar("1日分のデータを読み終わりました");
1250 private boolean updatePeriodList(){
1251 updateStatusBar("村情報を読み直しています…");
1253 Village.updateVillage(village);
1254 }catch(IOException e){
1255 showNetworkError(village, e);
1259 SwingUtilities.invokeAndWait(new Runnable(){
1261 tabBrowser.setVillage(village);
1265 }catch(InvocationTargetException e){
1267 "タブ操作で致命的な障害が発生しました", e);
1268 }catch(InterruptedException e){
1270 "タブ操作で致命的な障害が発生しました", e);
1272 updateStatusBar("村情報を読み直しました…");
1276 private void renderBrowser(){
1277 updateStatusBar("レンダリング中…");
1279 final int lastPos = periodView.getVerticalPosition();
1281 SwingUtilities.invokeAndWait(new Runnable(){
1283 periodView.showTopics();
1287 }catch(InvocationTargetException e){
1289 "ブラウザ表示で致命的な障害が発生しました", e);
1290 }catch(InterruptedException e){
1292 "ブラウザ表示で致命的な障害が発生しました", e);
1294 EventQueue.invokeLater(new Runnable(){
1296 periodView.setVerticalPosition(lastPos);
1300 updateStatusBar("レンダリング完了");
1312 private void filterChanged(){
1313 final Discussion discussion = currentDiscussion();
1314 if(discussion == null) return;
1315 discussion.setTopicFilter(this.filterFrame);
1317 Executor executor = Executors.newCachedThreadPool();
1318 executor.execute(new Runnable(){
1321 updateStatusBar("フィルタリング中…");
1323 discussion.filtering();
1325 updateStatusBar("フィルタリング完了");
1336 * 現在選択中のPeriodを内包するPeriodViewを返す。
1337 * @return PeriodView
1339 private PeriodView currentPeriodView(){
1340 TabBrowser tb = this.topView.getTabBrowser();
1341 PeriodView result = tb.currentPeriodView();
1346 * 現在選択中のPeriodを内包するDiscussionを返す。
1347 * @return Discussion
1349 private Discussion currentDiscussion(){
1350 PeriodView periodView = currentPeriodView();
1351 if(periodView == null) return null;
1352 Discussion result = periodView.getDiscussion();
1358 * @param window フレーム
1360 private void toggleWindow(Window window){
1361 if(window == null) return;
1363 if(window instanceof Frame){
1364 Frame frame = (Frame) window;
1365 int winState = frame.getExtendedState();
1366 boolean isIconified = (winState & Frame.ICONIFIED) != 0;
1368 winState &= ~(Frame.ICONIFIED);
1369 frame.setExtendedState(winState);
1370 frame.setVisible(true);
1375 if(window.isVisible()){
1376 window.setVisible(false);
1379 window.setVisible(true);
1385 * ネットワークエラーを通知するモーダルダイアログを表示する。
1386 * OKボタンを押すまでこのメソッドは戻ってこない。
1388 * @param e ネットワークエラー
1390 public void showNetworkError(Village village, IOException e){
1391 Land land = village.getParentLand();
1392 showNetworkError(land, e);
1397 * ネットワークエラーを通知するモーダルダイアログを表示する。
1398 * OKボタンを押すまでこのメソッドは戻ってこない。
1400 * @param e ネットワークエラー
1402 public void showNetworkError(Land land, IOException e){
1403 LOGGER.warn("ネットワークで障害が発生しました", e);
1405 ServerAccess server = land.getServerAccess();
1407 land.getLandDef().getLandName()
1409 +"何らかのトラブルが発生しました。\n"
1410 +"相手サーバのURLは [ " + server.getBaseURL() + " ] だよ。\n"
1412 +"Webブラウザでも遊べないか確認してみてね!\n";
1414 JOptionPane pane = new JOptionPane(message,
1415 JOptionPane.WARNING_MESSAGE,
1416 JOptionPane.DEFAULT_OPTION );
1418 String title = VerInfo.getFrameTitle("通信異常発生");
1419 JDialog dialog = pane.createDialog(this.topFrame, title);
1422 dialog.setVisible(true);
1430 * ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
1431 * @param event イベント {@inheritDoc}
1434 public void valueChanged(TreeSelectionEvent event){
1435 TreePath path = event.getNewLeadSelectionPath();
1436 if(path == null) return;
1438 Object selObj = path.getLastPathComponent();
1440 if( selObj instanceof Land ){
1441 Land land = (Land)selObj;
1442 setFrameTitle(land.getLandDef().getLandName());
1443 this.topView.showLandInfo(land);
1444 this.actionManager.appearVillage(false);
1445 this.actionManager.appearPeriod(false);
1446 }else if( selObj instanceof Village ){
1447 final Village village = (Village)selObj;
1449 Executor executor = Executors.newCachedThreadPool();
1450 executor.execute(new Runnable(){
1453 updateStatusBar("村情報を読み込み中…");
1456 Village.updateVillage(village);
1457 }catch(IOException e){
1458 showNetworkError(village, e);
1461 updateStatusBar("村情報の読み込み完了");
1465 Controller.this.actionManager.appearVillage(true);
1466 setFrameTitle(village.getVillageFullName());
1468 EventQueue.invokeLater(new Runnable(){
1470 Controller.this.topView.showVillageInfo(village);
1485 * Periodがタブ選択されたときもしくは発言フィルタが操作されたときの処理。
1486 * @param event イベント {@inheritDoc}
1489 public void stateChanged(ChangeEvent event){
1490 Object source = event.getSource();
1492 if(source == this.filterFrame){
1494 }else if(source instanceof TabBrowser){
1496 updatePeriod(false);
1497 PeriodView periodView = currentPeriodView();
1498 if(periodView == null) this.actionManager.appearPeriod(false);
1499 else this.actionManager.appearPeriod(true);
1507 * @param e イベント {@inheritDoc}
1510 public void actionPerformed(ActionEvent e){
1511 if(this.isBusyNow) return;
1513 String cmd = e.getActionCommand();
1514 if(cmd.equals(ActionManager.CMD_ACCOUNT)){
1515 actionShowAccount();
1516 }else if(cmd.equals(ActionManager.CMD_EXIT)){
1518 }else if(cmd.equals(ActionManager.CMD_COPY)){
1519 actionCopySelected();
1520 }else if(cmd.equals(ActionManager.CMD_SHOWFIND)){
1522 }else if(cmd.equals(ActionManager.CMD_SEARCHNEXT)){
1524 }else if(cmd.equals(ActionManager.CMD_SEARCHPREV)){
1526 }else if(cmd.equals(ActionManager.CMD_ALLPERIOD)){
1527 actionLoadAllPeriod();
1528 }else if(cmd.equals(ActionManager.CMD_SHOWDIGEST)){
1530 }else if(cmd.equals(ActionManager.CMD_WEBVILL)){
1531 actionShowWebVillage();
1532 }else if(cmd.equals(ActionManager.CMD_WEBWIKI)){
1533 actionShowWebWiki();
1534 }else if(cmd.equals(ActionManager.CMD_WEBCAST)){
1535 actionShowWebCast();
1536 }else if(cmd.equals(ActionManager.CMD_RELOAD)){
1537 actionReloadPeriod();
1538 }else if(cmd.equals(ActionManager.CMD_DAYSUMMARY)){
1540 }else if(cmd.equals(ActionManager.CMD_DAYEXPCSV)){
1541 actionDayExportCsv();
1542 }else if(cmd.equals(ActionManager.CMD_WEBDAY)){
1544 }else if(cmd.equals(ActionManager.CMD_OPTION)){
1546 }else if(cmd.equals(ActionManager.CMD_LANDF)){
1548 }else if(cmd.equals(ActionManager.CMD_SHOWFILT)){
1550 }else if(cmd.equals(ActionManager.CMD_SHOWEDIT)){
1551 actionTalkPreview();
1552 }else if(cmd.equals(ActionManager.CMD_SHOWLOG)){
1554 }else if(cmd.equals(ActionManager.CMD_HELPDOC)){
1556 }else if(cmd.equals(ActionManager.CMD_SHOWPORTAL)){
1558 }else if(cmd.equals(ActionManager.CMD_ABOUT)){
1560 }else if(cmd.equals(ActionManager.CMD_VILLAGELIST)){
1561 actionReloadVillageList();
1562 }else if(cmd.equals(ActionManager.CMD_COPYTALK)){
1564 }else if(cmd.equals(ActionManager.CMD_JUMPANCHOR)){
1566 }else if(cmd.equals(ActionManager.CMD_WEBTALK)){
1567 actionShowWebTalk();
1574 * 村選択ツリーリストが畳まれるとき呼ばれる。
1575 * @param event ツリーイベント {@inheritDoc}
1578 public void treeWillCollapse(TreeExpansionEvent event){
1584 * 村選択ツリーリストが展開されるとき呼ばれる。
1585 * @param event ツリーイベント {@inheritDoc}
1588 public void treeWillExpand(TreeExpansionEvent event){
1589 if(!(event.getSource() instanceof JTree)){
1593 TreePath path = event.getPath();
1594 Object lastObj = path.getLastPathComponent();
1595 if(!(lastObj instanceof Land)){
1598 final Land land = (Land) lastObj;
1599 if(land.getVillageCount() > 0){
1603 execReloadVillageList(land);
1610 * @param event {@inheritDoc}
1613 public void anchorHitted(AnchorHitEvent event){
1614 PeriodView periodView = currentPeriodView();
1615 if(periodView == null) return;
1616 Period period = periodView.getPeriod();
1617 if(period == null) return;
1618 final Village village = period.getVillage();
1620 final TalkDraw talkDraw = event.getTalkDraw();
1621 final Anchor anchor = event.getAnchor();
1622 final Discussion discussion = periodView.getDiscussion();
1624 Executor executor = Executors.newCachedThreadPool();
1625 executor.execute(new Runnable(){
1628 updateStatusBar("アンカーの展開中…");
1630 if(anchor.hasTalkNo()){
1632 taskLoadAllPeriod();
1635 final List<Talk> talkList;
1637 talkList = village.getTalkListFromAnchor(anchor);
1638 if(talkList == null || talkList.size() <= 0){
1645 EventQueue.invokeLater(new Runnable(){
1647 talkDraw.showAnchorTalks(anchor, talkList);
1648 discussion.layoutRows();
1656 }catch(IOException e){
1658 "アンカーの展開中にエラーが起きました");
1672 * プログレスバーとカーソルの設定を行う。
1673 * @param isBusy trueならプログレスバーのアニメ開始&WAITカーソル。
1676 private void setBusy(final boolean isBusy){
1677 this.isBusyNow = isBusy;
1679 Runnable microJob = new Runnable(){
1683 cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
1685 cursor = Cursor.getDefaultCursor();
1688 Component glass = Controller.this.topFrame.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 e){
1703 LOGGER.fatal("ビジー処理で失敗", e);
1704 }catch(InterruptedException e){
1705 LOGGER.fatal("ビジー処理で失敗", e);
1714 * @param message メッセージ
1716 private void updateStatusBar(String message){
1717 this.topView.updateSysMessage(message);
1721 * トップフレームのタイトルを設定する。
1722 * タイトルは指定された国or村名 + " - Jindolf"
1725 private void setFrameTitle(String name){
1726 String title = VerInfo.getFrameTitle(name);
1727 this.topFrame.setTitle(title);
1734 private void shutdown(){
1735 ConfigStore configStore = this.appSetting.getConfigStore();
1737 JsObject findConf = this.findPanel.getJson();
1738 if( ! this.findPanel.hasConfChanged(findConf) ){
1739 configStore.saveHistoryConfig(findConf);
1742 JsObject draftConf = this.talkPreview.getJson();
1743 if( ! this.talkPreview.hasConfChanged(draftConf) ){
1744 configStore.saveDraftConfig(draftConf);
1747 this.appSetting.saveConfig();
1749 LOGGER.info("VMごとアプリケーションを終了します。");
1750 System.exit(0); // invoke shutdown hooks... BYE !