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;
20 import java.text.MessageFormat;
21 import java.util.List;
22 import java.util.logging.Handler;
23 import java.util.logging.Level;
24 import java.util.logging.Logger;
25 import java.util.regex.Pattern;
26 import javax.swing.JButton;
27 import javax.swing.JDialog;
28 import javax.swing.JFileChooser;
29 import javax.swing.JOptionPane;
30 import javax.swing.JToolBar;
31 import javax.swing.JTree;
32 import javax.swing.UnsupportedLookAndFeelException;
33 import javax.swing.WindowConstants;
34 import javax.swing.event.ChangeEvent;
35 import javax.swing.event.ChangeListener;
36 import javax.swing.event.TreeExpansionEvent;
37 import javax.swing.event.TreeSelectionEvent;
38 import javax.swing.event.TreeSelectionListener;
39 import javax.swing.event.TreeWillExpandListener;
40 import javax.swing.filechooser.FileFilter;
41 import javax.swing.filechooser.FileNameExtensionFilter;
42 import javax.swing.tree.TreePath;
43 import jp.sfjp.jindolf.config.AppSetting;
44 import jp.sfjp.jindolf.config.ConfigStore;
45 import jp.sfjp.jindolf.config.JsonIo;
46 import jp.sfjp.jindolf.config.OptionInfo;
47 import jp.sfjp.jindolf.data.Anchor;
48 import jp.sfjp.jindolf.data.DialogPref;
49 import jp.sfjp.jindolf.data.Land;
50 import jp.sfjp.jindolf.data.LandsTreeModel;
51 import jp.sfjp.jindolf.data.Period;
52 import jp.sfjp.jindolf.data.RegexPattern;
53 import jp.sfjp.jindolf.data.Talk;
54 import jp.sfjp.jindolf.data.Village;
55 import jp.sfjp.jindolf.data.html.PeriodLoader;
56 import jp.sfjp.jindolf.data.html.VillageInfoLoader;
57 import jp.sfjp.jindolf.data.html.VillageListLoader;
58 import jp.sfjp.jindolf.data.xml.VillageLoader;
59 import jp.sfjp.jindolf.dxchg.CsvExporter;
60 import jp.sfjp.jindolf.dxchg.WebIPCDialog;
61 import jp.sfjp.jindolf.dxchg.WolfBBS;
62 import jp.sfjp.jindolf.glyph.AnchorHitEvent;
63 import jp.sfjp.jindolf.glyph.AnchorHitListener;
64 import jp.sfjp.jindolf.glyph.Discussion;
65 import jp.sfjp.jindolf.glyph.FontChooser;
66 import jp.sfjp.jindolf.glyph.FontInfo;
67 import jp.sfjp.jindolf.glyph.TalkDraw;
68 import jp.sfjp.jindolf.log.LogFrame;
69 import jp.sfjp.jindolf.log.LogUtils;
70 import jp.sfjp.jindolf.net.ProxyInfo;
71 import jp.sfjp.jindolf.net.ServerAccess;
72 import jp.sfjp.jindolf.summary.DaySummary;
73 import jp.sfjp.jindolf.summary.VillageDigest;
74 import jp.sfjp.jindolf.util.GUIUtils;
75 import jp.sfjp.jindolf.util.StringUtils;
76 import jp.sfjp.jindolf.view.ActionManager;
77 import jp.sfjp.jindolf.view.AvatarPics;
78 import jp.sfjp.jindolf.view.FilterPanel;
79 import jp.sfjp.jindolf.view.FindPanel;
80 import jp.sfjp.jindolf.view.HelpFrame;
81 import jp.sfjp.jindolf.view.LandsTree;
82 import jp.sfjp.jindolf.view.OptionPanel;
83 import jp.sfjp.jindolf.view.PeriodView;
84 import jp.sfjp.jindolf.view.TabBrowser;
85 import jp.sfjp.jindolf.view.TopFrame;
86 import jp.sfjp.jindolf.view.TopView;
87 import jp.sfjp.jindolf.view.WindowManager;
88 import jp.sourceforge.jindolf.corelib.VillageState;
89 import jp.sourceforge.jovsonz.JsObject;
90 import org.xml.sax.SAXException;
93 * いわゆるMVCでいうとこのコントローラ。
95 public class Controller
96 implements ActionListener,
98 private static final Logger LOGGER = Logger.getAnonymousLogger();
100 private static final String ERRTITLE_LAF = "Look&Feel";
101 private static final String ERRFORM_LAFGEN =
102 "このLook&Feel[{0}]を生成する事ができません。";
105 private final LandsTreeModel model;
106 private final WindowManager windowManager;
107 private final ActionManager actionManager;
108 private final AppSetting appSetting;
110 private final TopView topView;
112 private final JFileChooser xmlFileChooser = buildFileChooser();
114 private final VillageTreeWatcher treeVillageWatcher =
115 new VillageTreeWatcher();
116 private final ChangeListener tabPeriodWatcher =
117 new TabPeriodWatcher();
118 private final ChangeListener filterWatcher =
121 private final BusyStatus busyStatus;
126 * @param model 最上位データモデル
127 * @param windowManager ウィンドウ管理
128 * @param actionManager アクション管理
129 * @param setting アプリ設定
131 @SuppressWarnings("LeakingThisInConstructor")
132 public Controller(LandsTreeModel model,
133 WindowManager windowManager,
134 ActionManager actionManager,
138 this.appSetting = setting;
139 this.actionManager = actionManager;
140 this.windowManager = windowManager;
143 this.topView = this.windowManager.getTopFrame().getTopView();
145 JToolBar toolbar = this.actionManager.getBrowseToolBar();
146 this.topView.setBrowseToolBar(toolbar);
148 this.actionManager.addActionListener(this);
150 JTree treeView = this.topView.getTreeView();
151 treeView.setModel(this.model);
152 treeView.addTreeWillExpandListener(this.treeVillageWatcher);
153 treeView.addTreeSelectionListener(this.treeVillageWatcher);
155 TabBrowser periodTab = this.topView.getTabBrowser();
156 periodTab.addChangeListener(this.tabPeriodWatcher);
157 periodTab.addActionListener(this);
158 periodTab.addAnchorHitListener(this);
160 JButton reloadVillageListButton = this.topView
162 .getReloadVillageListButton();
163 reloadVillageListButton.addActionListener(this);
164 reloadVillageListButton.setEnabled(false);
166 TopFrame topFrame = this.windowManager.getTopFrame();
167 OptionPanel optionPanel = this.windowManager.getOptionPanel();
168 FindPanel findPanel = this.windowManager.getFindPanel();
169 FilterPanel filterPanel = this.windowManager.getFilterPanel();
170 LogFrame logFrame = this.windowManager.getLogFrame();
171 HelpFrame helpFrame = this.windowManager.getHelpFrame();
173 topFrame.setJMenuBar(this.actionManager.getMenuBar());
175 topFrame.setDefaultCloseOperation(
176 WindowConstants.DISPOSE_ON_CLOSE);
177 topFrame.addWindowListener(new WindowAdapter(){
180 public void windowClosed(WindowEvent event){
184 this.busyStatus = new BusyStatus(topFrame);
186 filterPanel.addChangeListener(this.filterWatcher);
188 Handler newHandler = logFrame.getHandler();
189 EventQueue.invokeLater(() -> {
190 LogUtils.switchHandler(newHandler);
193 JsonIo jsonIo = this.appSetting.getJsonIo();
195 JsObject history = jsonIo.loadHistoryConfig();
196 findPanel.putJson(history);
198 FontInfo fontInfo = this.appSetting.getFontInfo();
199 periodTab.setFontInfo(fontInfo);
200 optionPanel.getFontChooser().setFontInfo(fontInfo);
202 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
203 optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
205 DialogPref pref = this.appSetting.getDialogPref();
206 periodTab.setDialogPref(pref);
207 optionPanel.getDialogPrefPanel().setDialogPref(pref);
209 OptionInfo optInfo = this.appSetting.getOptionInfo();
210 ConfigStore configStore = this.appSetting.getConfigStore();
211 helpFrame.updateVmInfo(optInfo, configStore);
221 private static void toggleWindow(Window window){
222 if(window == null) return;
224 if(window instanceof Frame){
225 Frame frame = (Frame) window;
226 int winState = frame.getExtendedState();
227 boolean isIconified = (winState & Frame.ICONIFIED) != 0;
229 winState &= ~(Frame.ICONIFIED);
230 frame.setExtendedState(winState);
231 frame.setVisible(true);
236 if(window.isVisible()){
237 window.setVisible(false);
240 window.setVisible(true);
246 * XMLファイルを選択するためのChooserを生成する。
250 private static JFileChooser buildFileChooser(){
251 JFileChooser chooser = new JFileChooser();
252 chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
255 filter = new FileNameExtensionFilter("XML files (*.xml)", "xml", "XML");
256 chooser.setFileFilter(filter);
258 chooser.setDialogTitle("アーカイブXMLファイルを開く");
268 public WindowManager getWindowManager(){
269 return this.windowManager;
276 public TopFrame getTopFrame(){
277 TopFrame result = this.windowManager.getTopFrame();
283 * タイトルは指定された国or村名 + " - Jindolf"
286 private void setFrameTitle(String name){
287 String title = VerInfo.getFrameTitle(name);
288 TopFrame topFrame = this.windowManager.getTopFrame();
289 topFrame.setTitle(title);
294 * 現在選択中のPeriodを内包するPeriodViewを返す。
297 private PeriodView currentPeriodView(){
298 TabBrowser tb = this.topView.getTabBrowser();
299 PeriodView result = tb.currentPeriodView();
304 * 現在選択中のPeriodを内包するDiscussionを返す。
307 private Discussion currentDiscussion(){
308 PeriodView periodView = currentPeriodView();
309 if(periodView == null) return null;
310 Discussion result = periodView.getDiscussion();
317 * @return 選択中の村。なければnull。
319 private Village getVillage(){
320 TabBrowser browser = this.topView.getTabBrowser();
321 Village village = browser.getVillage();
327 * @param message メッセージ
329 private void updateStatusBar(String message){
330 this.topView.updateSysMessage(message);
337 private void actionAbout(){
338 String message = VerInfo.getAboutMessage();
339 JOptionPane pane = new JOptionPane(message,
340 JOptionPane.INFORMATION_MESSAGE,
341 JOptionPane.DEFAULT_OPTION,
342 GUIUtils.getLogoIcon());
344 JDialog dialog = pane.createDialog(VerInfo.TITLE + "について");
347 dialog.setVisible(true);
356 private void actionExit(){
364 private void actionHelp(){
365 HelpFrame helpFrame = this.windowManager.getHelpFrame();
366 toggleWindow(helpFrame);
373 private void actionShowWebVillage(){
374 Village village = getVillage();
375 if(village == null) return;
377 Land land = village.getParentLand();
378 ServerAccess server = land.getServerAccess();
380 URL url = server.getVillageURL(village);
382 String urlText = url.toString();
383 if(village.getState() != VillageState.GAMEOVER){
384 urlText += "#bottom";
387 WebIPCDialog.showDialog(getTopFrame(), urlText);
393 * 村に対応するまとめサイトをWebブラウザで表示する。
395 private void actionShowWebWiki(){
396 Village village = getVillage();
397 if(village == null) return;
399 String urlTxt = WolfBBS.getCastGeneratorUrl(village);
400 WebIPCDialog.showDialog(getTopFrame(), urlTxt);
406 * 日(Period)をWebブラウザで表示する。
408 private void actionShowWebDay(){
409 PeriodView periodView = currentPeriodView();
410 if(periodView == null) return;
412 Period period = periodView.getPeriod();
413 if(period == null) return;
415 Village village = getVillage();
416 if(village == null) return;
418 Land land = village.getParentLand();
419 ServerAccess server = land.getServerAccess();
421 URL url = server.getPeriodURL(period);
423 String urlText = url.toString();
425 WebIPCDialog.showDialog(getTopFrame(), urlText);
431 * 個別の発言をWebブラウザで表示する。
433 private void actionShowWebTalk(){
434 Village village = getVillage();
435 if(village == null) return;
437 PeriodView periodView = currentPeriodView();
438 if(periodView == null) return;
440 Discussion discussion = periodView.getDiscussion();
441 Talk talk = discussion.getActiveTalk();
442 if(talk == null) return;
444 Period period = periodView.getPeriod();
445 if(period == null) return;
447 Land land = village.getParentLand();
448 ServerAccess server = land.getServerAccess();
450 URL url = server.getPeriodURL(period);
452 String urlText = url.toString();
453 urlText += "#" + talk.getMessageID();
454 WebIPCDialog.showDialog(getTopFrame(), urlText);
460 * ポータルサイトをWebブラウザで表示する。
462 private void actionShowPortal(){
463 WebIPCDialog.showDialog(getTopFrame(), VerInfo.CONTACT);
468 * 例外発生による警告ダイアログへの反応を促す。
469 * @param title タイトル文字列
470 * @param message メッセージ
473 private void warnDialog(String title, String message, Throwable e){
474 LOGGER.log(Level.WARNING, message, e);
475 JOptionPane.showMessageDialog(
478 VerInfo.getFrameTitle(title),
479 JOptionPane.WARNING_MESSAGE );
486 private void actionChangeLaF(){
487 String className = this.actionManager.getSelectedLookAndFeel();
488 if(className == null) return;
490 this.busyStatus.submitLightBusyTask(
491 () -> {taskChangeLaF(className);},
500 * LookAndFeelの実際の更新を行う軽量タスク。
502 * @param lnf LookAndFeel
504 private void taskChangeLaF(String className){
505 assert EventQueue.isDispatchThread();
508 this.windowManager.changeAllWindowUI(className);
509 }catch(UnsupportedLookAndFeelException e){
510 String warnMsg = MessageFormat.format(
511 "このLook&Feel[{0}]はサポートされていません。",
513 warnDialog(ERRTITLE_LAF, warnMsg, e);
515 }catch(ReflectiveOperationException e){
516 String warnMsg = MessageFormat.format(ERRFORM_LAFGEN, className);
517 warnDialog(ERRTITLE_LAF, warnMsg, e);
521 this.xmlFileChooser.updateUI();
523 LOGGER.log(Level.INFO,
524 "Look&Feelが[{0}]に変更されました。", className );
532 private void actionShowFilter(){
533 FilterPanel filterPanel = this.windowManager.getFilterPanel();
534 toggleWindow(filterPanel);
541 private void actionShowLog(){
542 LogFrame logFrame = this.windowManager.getLogFrame();
543 toggleWindow(logFrame);
550 private void actionOption(){
551 OptionPanel optionPanel = this.windowManager.getOptionPanel();
553 FontInfo fontInfo = this.appSetting.getFontInfo();
554 optionPanel.getFontChooser().setFontInfo(fontInfo);
556 ProxyInfo proxyInfo = this.appSetting.getProxyInfo();
557 optionPanel.getProxyChooser().setProxyInfo(proxyInfo);
559 DialogPref dialogPref = this.appSetting.getDialogPref();
560 optionPanel.getDialogPrefPanel().setDialogPref(dialogPref);
562 optionPanel.setVisible(true);
563 if(optionPanel.isCanceled()) return;
565 fontInfo = optionPanel.getFontChooser().getFontInfo();
566 updateFontInfo(fontInfo);
568 proxyInfo = optionPanel.getProxyChooser().getProxyInfo();
569 updateProxyInfo(proxyInfo);
571 dialogPref = optionPanel.getDialogPrefPanel().getDialogPref();
572 updateDialogPref(dialogPref);
579 * @param newFontInfo 新フォント設定
581 private void updateFontInfo(final FontInfo newFontInfo){
582 FontInfo oldInfo = this.appSetting.getFontInfo();
584 if(newFontInfo.equals(oldInfo)) return;
585 this.appSetting.setFontInfo(newFontInfo);
587 this.topView.getTabBrowser().setFontInfo(newFontInfo);
589 OptionPanel optionPanel = this.windowManager.getOptionPanel();
590 FontChooser fontChooser = optionPanel.getFontChooser();
592 fontChooser.setFontInfo(newFontInfo);
599 * @param newProxyInfo 新プロクシ設定
601 private void updateProxyInfo(ProxyInfo newProxyInfo){
602 ProxyInfo oldProxyInfo = this.appSetting.getProxyInfo();
604 if(newProxyInfo.equals(oldProxyInfo)) return;
605 this.appSetting.setProxyInfo(newProxyInfo);
607 for(Land land : this.model.getLandList()){
608 ServerAccess server = land.getServerAccess();
609 server.setProxy(newProxyInfo.getProxy());
617 * @param newDialogPref 表示設定
619 private void updateDialogPref(DialogPref newDialogPref){
620 DialogPref oldDialogPref = this.appSetting.getDialogPref();
622 if(newDialogPref.equals(oldDialogPref)) return;
623 this.appSetting.setDialogPref(newDialogPref);
625 this.topView.getTabBrowser().setDialogPref(newDialogPref);
633 private void actionShowDigest(){
634 Village village = getVillage();
635 if(village == null) return;
637 VillageState villageState = village.getState();
638 if( ( villageState != VillageState.EPILOGUE
639 && villageState != VillageState.GAMEOVER
640 ) || ! village.isValid() ){
641 String message = "エピローグを正常に迎えていない村は\n"
643 String title = VerInfo.getFrameTitle("ダイジェスト不可");
644 JOptionPane pane = new JOptionPane(message,
645 JOptionPane.WARNING_MESSAGE,
646 JOptionPane.DEFAULT_OPTION );
647 JDialog dialog = pane.createDialog(title);
649 dialog.setVisible(true);
654 VillageDigest villageDigest = this.windowManager.getVillageDigest();
655 final VillageDigest digest = villageDigest;
657 Runnable task = () -> {
658 taskFullOpenAllPeriod();
659 EventQueue.invokeLater(() -> {
660 digest.setVillage(village);
661 digest.setVisible(true);
665 this.busyStatus.submitHeavyBusyTask(
675 * 全日程の一括フルオープン。ヘビータスク版。
677 // TODO taskLoadAllPeriodtと一体化したい。
678 private void taskFullOpenAllPeriod(){
679 TabBrowser browser = this.topView.getTabBrowser();
680 Village village = getVillage();
681 if(village == null) return;
682 for(PeriodView periodView : browser.getPeriodViewList()){
683 Period period = periodView.getPeriod();
684 if(period == null) continue;
688 updateStatusBar(message);
690 PeriodLoader.parsePeriod(period, false);
691 }catch(IOException e){
692 showNetworkError(village, e);
695 periodView.showTopics();
704 private void actionShowFind(){
705 FindPanel findPanel = this.windowManager.getFindPanel();
707 findPanel.setVisible(true);
708 if(findPanel.isCanceled()){
712 if(findPanel.isBulkSearch()){
723 private void regexSearch(){
724 Discussion discussion = currentDiscussion();
725 if(discussion == null) return;
727 FindPanel findPanel = this.windowManager.getFindPanel();
728 RegexPattern regPattern = findPanel.getRegexPattern();
729 int hits = discussion.setRegexPattern(regPattern);
731 String hitMessage = "[" + hits + "]件ヒットしました";
732 updateStatusBar(hitMessage);
735 if(regPattern != null){
736 Pattern pattern = regPattern.getPattern();
738 loginfo = "正規表現 " + pattern.pattern() + " に";
741 loginfo += hitMessage;
742 LOGGER.info(loginfo);
750 private void bulkSearch(){
751 this.busyStatus.submitHeavyBusyTask(
752 () -> {taskBulkSearch();},
761 private void taskBulkSearch(){
764 FindPanel findPanel = this.windowManager.getFindPanel();
765 RegexPattern regPattern = findPanel.getRegexPattern();
766 StringBuilder hitDesc = new StringBuilder();
767 TabBrowser browser = this.topView.getTabBrowser();
768 for(PeriodView periodView : browser.getPeriodViewList()){
769 Discussion discussion = periodView.getDiscussion();
770 int hits = discussion.setRegexPattern(regPattern);
774 Period period = discussion.getPeriod();
775 hitDesc.append(' ').append(period.getDay()).append("d:");
776 hitDesc.append(hits).append("件");
780 "[" + totalhits + "]件ヒットしました。"
781 + hitDesc.toString();
782 updateStatusBar(hitMessage);
785 if(regPattern != null){
786 Pattern pattern = regPattern.getPattern();
788 loginfo = "正規表現 " + pattern.pattern() + " に";
791 loginfo += hitMessage;
792 LOGGER.info(loginfo);
798 * 検索パネルに現在選択中のPeriodを反映させる。
800 private void updateFindPanel(){
801 Discussion discussion = currentDiscussion();
802 if(discussion == null) return;
803 RegexPattern pattern = discussion.getRegexPattern();
804 FindPanel findPanel = this.windowManager.getFindPanel();
805 findPanel.setRegexPattern(pattern);
812 private void actionDaySummary(){
813 PeriodView periodView = currentPeriodView();
814 if(periodView == null) return;
816 Period period = periodView.getPeriod();
817 if(period == null) return;
819 DaySummary daySummary = this.windowManager.getDaySummary();
820 daySummary.summaryPeriod(period);
821 daySummary.setVisible(true);
827 * 表示中PeriodをCSVファイルへエクスポートする。
829 private void actionDayExportCsv(){
830 PeriodView periodView = currentPeriodView();
831 if(periodView == null) return;
833 Period period = periodView.getPeriod();
834 if(period == null) return;
836 FilterPanel filterPanel = this.windowManager.getFilterPanel();
837 File file = CsvExporter.exportPeriod(period, filterPanel);
839 String message = "CSVファイル("
842 updateStatusBar(message);
845 // TODO 長そうなジョブなら別スレッドにした方がいいか?
853 private void actionSearchNext(){
854 Discussion discussion = currentDiscussion();
855 if(discussion == null) return;
857 discussion.nextHotTarget();
865 private void actionSearchPrev(){
866 Discussion discussion = currentDiscussion();
867 if(discussion == null) return;
869 discussion.prevHotTarget();
877 private void actionReloadPeriod(){
880 Village village = getVillage();
881 if(village == null) return;
882 if(village.getState() != VillageState.EPILOGUE) return;
884 Discussion discussion = currentDiscussion();
885 if(discussion == null) return;
886 Period period = discussion.getPeriod();
887 if(period == null) return;
888 if(period.getTopics() > 1000){
889 JOptionPane.showMessageDialog(getTopFrame(),
890 "エピローグが1000発言を超えはじめたら、\n"
891 +"負荷対策のためWebブラウザによるアクセスを"
894 JOptionPane.WARNING_MESSAGE
904 private void actionLoadAllPeriod(){
905 this.busyStatus.submitHeavyBusyTask(
906 () -> {taskLoadAllPeriod();},
917 private void taskLoadAllPeriod(){
918 TabBrowser browser = this.topView.getTabBrowser();
919 Village village = getVillage();
920 if(village == null) return;
921 for(PeriodView periodView : browser.getPeriodViewList()){
922 Period period = periodView.getPeriod();
923 if(period == null) continue;
927 updateStatusBar(message);
929 PeriodLoader.parsePeriod(period, false);
930 }catch(IOException e){
931 showNetworkError(village, e);
934 periodView.showTopics();
943 private void actionReloadVillageList(){
944 JTree tree = this.topView.getTreeView();
945 TreePath path = tree.getSelectionPath();
946 if(path == null) return;
949 for(int ct = 0; ct < path.getPathCount(); ct++){
950 Object obj = path.getPathComponent(ct);
951 if(obj instanceof Land){
956 if(land == null) return;
958 this.topView.showInitPanel();
960 submitReloadVillageList(land);
966 * 選択文字列をクリップボードにコピーする。
968 private void actionCopySelected(){
969 Discussion discussion = currentDiscussion();
970 if(discussion == null) return;
972 CharSequence copied = discussion.copySelected();
973 if(copied == null) return;
975 copied = StringUtils.suppressString(copied);
977 "[" + copied + "]をクリップボードにコピーしました");
982 * 一発言のみクリップボードにコピーする。
984 private void actionCopyTalk(){
985 Discussion discussion = currentDiscussion();
986 if(discussion == null) return;
988 CharSequence copied = discussion.copyTalk();
989 if(copied == null) return;
991 copied = StringUtils.suppressString(copied);
993 "[" + copied + "]をクリップボードにコピーしました");
998 * アンカー先を含むPeriodの全会話を事前にロードする。
1001 * @param anchor アンカー
1002 * @return アンカー先を含むPeriod。
1003 * アンカーがG国発言番号ならnull。
1004 * Periodが見つからないならnull。
1005 * @throws IOException 入力エラー
1007 private Period loadAnchoredPeriod(Village village, Anchor anchor)
1009 if(anchor.hasTalkNo()) return null;
1011 Period anchorPeriod = village.getPeriod(anchor);
1012 if(anchorPeriod == null) return null;
1014 PeriodLoader.parsePeriod(anchorPeriod, false);
1016 return anchorPeriod;
1022 private void actionJumpAnchor(){
1023 PeriodView periodView = currentPeriodView();
1024 if(periodView == null) return;
1025 Discussion discussion = periodView.getDiscussion();
1027 TabBrowser browser = this.topView.getTabBrowser();
1028 Village village = getVillage();
1029 final Anchor anchor = discussion.getActiveAnchor();
1030 if(anchor == null) return;
1032 Runnable task = () -> {
1033 if(anchor.hasTalkNo()){
1035 taskLoadAllPeriod();
1038 final List<Talk> talkList;
1040 loadAnchoredPeriod(village, anchor);
1041 talkList = village.getTalkListFromAnchor(anchor);
1042 if(talkList == null || talkList.size() <= 0){
1050 Talk targetTalk = talkList.get(0);
1051 Period targetPeriod = targetTalk.getPeriod();
1052 int periodIndex = targetPeriod.getDay();
1053 PeriodView target = browser.getPeriodView(periodIndex);
1055 EventQueue.invokeLater(() -> {
1056 browser.showPeriodTab(periodIndex);
1057 target.setPeriod(targetPeriod);
1058 target.scrollToTalk(targetTalk);
1064 }catch(IOException e){
1066 "アンカーの展開中にエラーが起きました");
1071 this.busyStatus.submitHeavyBusyTask(
1081 * ローカルなXMLファイルを読み込む。
1083 private void actionOpenXml(){
1084 int result = this.xmlFileChooser.showOpenDialog(getTopFrame());
1085 if(result != JFileChooser.APPROVE_OPTION) return;
1086 File selected = this.xmlFileChooser.getSelectedFile();
1088 this.busyStatus.submitHeavyBusyTask(() -> {
1092 village = VillageLoader.parseVillage(selected);
1093 }catch(IOException e){
1094 String warnMsg = MessageFormat.format(
1095 "XMLファイル[ {0} ]を読み込むことができません",
1098 warnDialog("XML I/O error", warnMsg, e);
1100 }catch(SAXException e){
1101 String warnMsg = MessageFormat.format(
1102 "XMLファイル[ {0} ]の形式が不正なため読み込むことができません",
1105 warnDialog("XML form error", warnMsg, e);
1109 village.setLocalArchive(true);
1110 AvatarPics avatarPics = village.getAvatarPics();
1111 this.appSetting.applyLocalImage(avatarPics);
1112 avatarPics.preload();
1113 EventQueue.invokeLater(() -> {
1114 selectedVillage(village);
1116 }, "XML読み込み中", "XML読み込み完了");
1122 * 指定した国の村一覧を読み込むジョブを投下。
1125 private void submitReloadVillageList(final Land land){
1126 this.busyStatus.submitHeavyBusyTask(
1127 () -> {taskReloadVillageList(land);},
1135 * 指定した国の村一覧を読み込む。(ヘビータスク本体).
1138 private void taskReloadVillageList(Land land){
1139 List<Village> villageList;
1141 villageList = VillageListLoader.loadVillageList(land);
1142 }catch(IOException e){
1143 showNetworkError(land, e);
1146 land.updateVillageList(villageList);
1148 this.model.updateVillageList(land);
1150 LandsTree treePanel = this.topView.getLandsTree();
1151 treePanel.expandLand(land);
1158 * @param force trueならPeriodデータを強制再読み込み。
1160 private void updatePeriod(final boolean force){
1161 Village village = getVillage();
1162 if(village == null) return;
1164 String fullName = village.getVillageFullName();
1165 setFrameTitle(fullName);
1167 PeriodView periodView = currentPeriodView();
1168 Discussion discussion = currentDiscussion();
1169 if(discussion == null) return;
1171 FilterPanel filterPanel = this.windowManager.getFilterPanel();
1172 discussion.setTopicFilter(filterPanel);
1174 Period period = discussion.getPeriod();
1175 if(period == null) return;
1177 Runnable task = () -> {
1179 PeriodLoader.parsePeriod(period, force);
1180 }catch(IOException e){
1181 showNetworkError(village, e);
1185 EventQueue.invokeLater(() -> {
1186 int lastPos = periodView.getVerticalPosition();
1187 periodView.showTopics();
1188 periodView.setVerticalPosition(lastPos);
1192 this.busyStatus.submitHeavyBusyTask(
1204 private void filterChanged(){
1205 final Discussion discussion = currentDiscussion();
1206 if(discussion == null) return;
1208 FilterPanel filterPanel = this.windowManager.getFilterPanel();
1210 discussion.setTopicFilter(filterPanel);
1211 discussion.filtering();
1217 * ネットワークエラーを通知するモーダルダイアログを表示する。
1218 * OKボタンを押すまでこのメソッドは戻ってこない。
1220 * @param e ネットワークエラー
1222 public void showNetworkError(Village village, IOException e){
1223 Land land = village.getParentLand();
1224 showNetworkError(land, e);
1229 * ネットワークエラーを通知するモーダルダイアログを表示する。
1230 * OKボタンを押すまでこのメソッドは戻ってこない。
1232 * @param e ネットワークエラー
1234 public void showNetworkError(Land land, IOException e){
1235 LOGGER.log(Level.WARNING, "ネットワークで障害が発生しました", e);
1237 ServerAccess server = land.getServerAccess();
1239 land.getLandDef().getLandName()
1241 +"何らかのトラブルが発生しました。\n"
1242 +"相手サーバのURLは [ " + server.getBaseURL() + " ] だよ。\n"
1244 +"Webブラウザでも遊べないか確認してみてね!\n";
1246 JOptionPane pane = new JOptionPane(message,
1247 JOptionPane.WARNING_MESSAGE,
1248 JOptionPane.DEFAULT_OPTION );
1250 String title = VerInfo.getFrameTitle("通信異常発生");
1251 JDialog dialog = pane.createDialog(title);
1254 dialog.setVisible(true);
1265 private void selectedLand(Land land){
1266 String landName = land.getLandDef().getLandName();
1267 setFrameTitle(landName);
1269 this.actionManager.exposeVillage(false);
1270 this.actionManager.exposePeriod(false);
1272 this.topView.showLandInfo(land);
1282 private void selectedVillage(Village village){
1283 setFrameTitle(village.getVillageFullName());
1284 if(village.isLocalArchive()){
1285 this.actionManager.exposeVillageLocal(true);
1287 this.actionManager.exposeVillage(true);
1290 Runnable task = () -> {
1292 if( ! village.hasSchedule() ){
1293 VillageInfoLoader.updateVillageInfo(village);
1295 }catch(IOException e){
1296 showNetworkError(village, e);
1300 EventQueue.invokeLater(() -> {
1301 this.topView.showVillageInfo(village);
1305 this.busyStatus.submitHeavyBusyTask(
1317 * <p>主にメニュー選択やボタン押下などのアクションをディスパッチする。
1321 * @param ev {@inheritDoc}
1324 public void actionPerformed(ActionEvent ev){
1325 if(this.busyStatus.isBusy()) return;
1327 String cmd = ev.getActionCommand();
1328 if(cmd == null) return;
1331 case ActionManager.CMD_OPENXML:
1334 case ActionManager.CMD_EXIT:
1337 case ActionManager.CMD_COPY:
1338 actionCopySelected();
1340 case ActionManager.CMD_SHOWFIND:
1343 case ActionManager.CMD_SEARCHNEXT:
1346 case ActionManager.CMD_SEARCHPREV:
1349 case ActionManager.CMD_ALLPERIOD:
1350 actionLoadAllPeriod();
1352 case ActionManager.CMD_SHOWDIGEST:
1355 case ActionManager.CMD_WEBVILL:
1356 actionShowWebVillage();
1358 case ActionManager.CMD_WEBWIKI:
1359 actionShowWebWiki();
1361 case ActionManager.CMD_RELOAD:
1362 actionReloadPeriod();
1364 case ActionManager.CMD_DAYSUMMARY:
1367 case ActionManager.CMD_DAYEXPCSV:
1368 actionDayExportCsv();
1370 case ActionManager.CMD_WEBDAY:
1373 case ActionManager.CMD_OPTION:
1376 case ActionManager.CMD_LANDF:
1379 case ActionManager.CMD_SHOWFILT:
1382 case ActionManager.CMD_SHOWLOG:
1385 case ActionManager.CMD_HELPDOC:
1388 case ActionManager.CMD_SHOWPORTAL:
1391 case ActionManager.CMD_ABOUT:
1394 case ActionManager.CMD_VILLAGELIST:
1395 actionReloadVillageList();
1397 case ActionManager.CMD_COPYTALK:
1400 case ActionManager.CMD_JUMPANCHOR:
1403 case ActionManager.CMD_WEBTALK:
1404 actionShowWebTalk();
1415 * @param event {@inheritDoc}
1418 public void anchorHitted(AnchorHitEvent event){
1419 PeriodView periodView = currentPeriodView();
1420 if(periodView == null) return;
1421 Period period = periodView.getPeriod();
1422 if(period == null) return;
1423 final Village village = period.getVillage();
1425 final TalkDraw talkDraw = event.getTalkDraw();
1426 final Anchor anchor = event.getAnchor();
1427 final Discussion discussion = periodView.getDiscussion();
1429 Runnable task = () -> {
1430 if(anchor.hasTalkNo()){
1432 taskLoadAllPeriod();
1435 final List<Talk> talkList;
1437 loadAnchoredPeriod(village, anchor);
1438 talkList = village.getTalkListFromAnchor(anchor);
1439 if(talkList == null || talkList.size() <= 0){
1446 EventQueue.invokeLater(() -> {
1447 talkDraw.showAnchorTalks(anchor, talkList);
1448 discussion.layoutRows();
1454 }catch(IOException e){
1456 "アンカーの展開中にエラーが起きました");
1460 this.busyStatus.submitHeavyBusyTask(
1472 private void shutdown(){
1473 JsonIo jsonIo = this.appSetting.getJsonIo();
1475 FindPanel findPanel = this.windowManager.getFindPanel();
1476 JsObject findConf = findPanel.getJson();
1477 if( ! findPanel.hasConfChanged(findConf) ){
1478 jsonIo.saveHistoryConfig(findConf);
1481 this.appSetting.saveConfig();
1483 LOGGER.info("VMごとアプリケーションを終了します。");
1484 System.exit(0); // invoke shutdown hooks... BYE !
1494 private class FilterWatcher implements ChangeListener{
1508 * <p>発言フィルタが操作されたときの処理。
1510 * @param event {@inheritDoc}
1513 public void stateChanged(ChangeEvent event){
1514 Object source = event.getSource();
1516 if(source == Controller.this.windowManager.getFilterPanel()){
1526 * Period一覧タブのタブ操作を監視する。
1528 private class TabPeriodWatcher implements ChangeListener{
1542 * <p>Periodがタブ選択されたときの処理。
1544 * @param event {@inheritDoc}
1547 public void stateChanged(ChangeEvent event){
1548 Object source = event.getSource();
1550 if(source instanceof TabBrowser){
1552 updatePeriod(false);
1553 PeriodView periodView = currentPeriodView();
1554 boolean hasCurrentPeriod;
1555 if(periodView == null) hasCurrentPeriod = false;
1556 else hasCurrentPeriod = true;
1557 Controller.this.actionManager.exposePeriod(hasCurrentPeriod);
1558 if(hasCurrentPeriod){
1559 Village village = getVillage();
1560 if(village.isLocalArchive()){
1561 Controller.this.actionManager.exposeVillageLocal(hasCurrentPeriod);
1563 Controller.this.actionManager.exposeVillage(hasCurrentPeriod);
1574 * 国村選択リストの選択展開操作を監視する。
1576 private class VillageTreeWatcher
1577 implements TreeSelectionListener, TreeWillExpandListener{
1582 VillageTreeWatcher(){
1591 * <p>ツリーリストで何らかの要素(国、村)がクリックされたときの処理。
1593 * @param event {@inheritDoc}
1596 public void valueChanged(TreeSelectionEvent event){
1597 TreePath path = event.getNewLeadSelectionPath();
1598 if(path == null) return;
1600 Object selObj = path.getLastPathComponent();
1601 if(selObj instanceof Land){
1602 Land land = (Land) selObj;
1604 }else if(selObj instanceof Village){
1605 Village village = (Village) selObj;
1606 village.setLocalArchive(false);
1607 selectedVillage(village);
1616 * <p>村選択ツリーリストが畳まれるとき呼ばれる。
1618 * @param event ツリーイベント {@inheritDoc}
1621 public void treeWillCollapse(TreeExpansionEvent event){
1628 * <p>村選択ツリーリストが展開されるとき呼ばれる。
1630 * @param event ツリーイベント {@inheritDoc}
1633 public void treeWillExpand(TreeExpansionEvent event){
1634 if(!(event.getSource() instanceof JTree)){
1638 TreePath path = event.getPath();
1639 Object lastObj = path.getLastPathComponent();
1640 if(!(lastObj instanceof Land)){
1643 Land land = (Land) lastObj;
1644 if(land.getVillageCount() > 0){
1648 submitReloadVillageList(land);