OSDN Git Service

Maven3対応。
[jindolf/Jindolf.git] / src / main / java / jp / sourceforge / jindolf / FindPanel.java
1 /*
2  * Find panel
3  *
4  * License : The MIT License
5  * Copyright(c) 2008 olyutorskii
6  */
7
8 package jp.sourceforge.jindolf;
9
10 import java.awt.BorderLayout;
11 import java.awt.Component;
12 import java.awt.Container;
13 import java.awt.Frame;
14 import java.awt.GridBagConstraints;
15 import java.awt.GridBagLayout;
16 import java.awt.GridLayout;
17 import java.awt.Insets;
18 import java.awt.event.ActionEvent;
19 import java.awt.event.ActionListener;
20 import java.awt.event.ItemEvent;
21 import java.awt.event.ItemListener;
22 import java.awt.event.WindowAdapter;
23 import java.awt.event.WindowEvent;
24 import java.beans.PropertyChangeEvent;
25 import java.beans.PropertyChangeListener;
26 import java.io.File;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.LinkedList;
30 import java.util.List;
31 import java.util.regex.Pattern;
32 import java.util.regex.PatternSyntaxException;
33 import javax.swing.BorderFactory;
34 import javax.swing.ComboBoxEditor;
35 import javax.swing.ComboBoxModel;
36 import javax.swing.DefaultListCellRenderer;
37 import javax.swing.Icon;
38 import javax.swing.JButton;
39 import javax.swing.JCheckBox;
40 import javax.swing.JComboBox;
41 import javax.swing.JDialog;
42 import javax.swing.JLabel;
43 import javax.swing.JList;
44 import javax.swing.JOptionPane;
45 import javax.swing.JPanel;
46 import javax.swing.JSeparator;
47 import javax.swing.border.Border;
48 import javax.swing.event.ChangeEvent;
49 import javax.swing.event.ChangeListener;
50 import javax.swing.event.EventListenerList;
51 import javax.swing.event.ListDataEvent;
52 import javax.swing.event.ListDataListener;
53 import javax.swing.text.JTextComponent;
54 import jp.sourceforge.jovsonz.JsArray;
55 import jp.sourceforge.jovsonz.JsObject;
56 import jp.sourceforge.jovsonz.JsValue;
57
58 /**
59  * 検索パネルGUI。
60  */
61 @SuppressWarnings("serial")
62 public class FindPanel extends JDialog
63         implements ActionListener,
64                    ItemListener,
65                    ChangeListener,
66                    PropertyChangeListener {
67
68     private static final String HIST_FILE = "searchHistory.json";
69     private static final String FRAMETITLE = "発言検索 - " + Jindolf.TITLE;
70     private static final String LABEL_REENTER = "再入力";
71     private static final String LABEL_IGNORE = "無視して検索をキャンセル";
72
73     private final JComboBox findBox = new JComboBox();
74     private final JButton searchButton = new JButton("検索");
75     private final JButton clearButton = new JButton("クリア");
76     private final JCheckBox capitalSwitch =
77             new JCheckBox("大文字/小文字を区別する");
78     private final JCheckBox regexSwitch =
79             new JCheckBox("正規表現");
80     private final JCheckBox dotallSwitch =
81             new JCheckBox("正規表現 \".\" を行末記号にもマッチさせる");
82     private final JCheckBox multilineSwitch =
83             new JCheckBox("正規表現 \"^\" や \"$\" を"
84                          +"行末記号の前後に反応させる");
85     private final JCheckBox bulkSearchSwitch =
86             new JCheckBox("全日程を一括検索");
87     private final JButton closeButton = new JButton("キャンセル");
88
89     private final CustomModel model = new CustomModel();
90
91     private JsObject loadedHistory = null;
92
93     private boolean canceled = false;
94     private RegexPattern regexPattern = null;
95
96     /**
97      * 検索パネルを生成する。
98      * @param owner 親フレーム。
99      */
100     public FindPanel(Frame owner){
101         super(owner, FRAMETITLE, true);
102
103         GUIUtils.modifyWindowAttributes(this, true, false, true);
104
105         setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
106         addWindowListener(new WindowAdapter(){
107             @Override
108             public void windowClosing(WindowEvent event){
109                 actionCancel();
110                 return;
111             }
112         });
113
114         design();
115
116         this.findBox.setModel(this.model);
117         this.findBox.setToolTipText("検索文字列を入力してください");
118         this.findBox.setEditable(true);
119         this.findBox.setRenderer(new CustomRenderer());
120         this.findBox.setMaximumRowCount(15);
121
122         ComboBoxEditor editor = this.findBox.getEditor();
123         modifyComboBoxEditor(editor);
124         this.findBox.addPropertyChangeListener("UI", this);
125
126         this.searchButton.setToolTipText("発言内容を検索する");
127         this.clearButton.setToolTipText("入力をクリアする");
128
129         this.findBox.addItemListener(this);
130         this.searchButton.addActionListener(this);
131         this.clearButton.addActionListener(this);
132         this.regexSwitch.addChangeListener(this);
133         this.closeButton.addActionListener(this);
134
135         setRegexPattern(null);
136
137         return;
138     }
139
140     /**
141      * デザイン・レイアウトを行う。
142      */
143     private void design(){
144         Container content = getContentPane();
145
146         GridBagLayout layout = new GridBagLayout();
147         GridBagConstraints constraints = new GridBagConstraints();
148
149         content.setLayout(layout);
150
151         constraints.insets = new Insets(2, 2, 2, 2);
152
153         constraints.weightx = 1.0;
154         constraints.fill = GridBagConstraints.HORIZONTAL;
155         constraints.gridwidth = 2;
156         Border border =
157                 BorderFactory
158                 .createTitledBorder("検索文字列を入力してください");
159         JPanel panel = new JPanel();
160         panel.setLayout(new BorderLayout());
161         panel.add(this.findBox, BorderLayout.CENTER);
162         panel.setBorder(border);
163         content.add(panel, constraints);
164
165         constraints.weightx = 0.0;
166         constraints.fill = GridBagConstraints.NONE;
167         constraints.gridwidth = GridBagConstraints.REMAINDER;
168         constraints.anchor = GridBagConstraints.SOUTH;
169         content.add(this.searchButton, constraints);
170
171         constraints.gridwidth = GridBagConstraints.REMAINDER;
172         constraints.anchor = GridBagConstraints.WEST;
173         content.add(this.clearButton, constraints);
174
175         constraints.gridwidth = GridBagConstraints.REMAINDER;
176         constraints.anchor = GridBagConstraints.WEST;
177         content.add(this.capitalSwitch, constraints);
178
179         constraints.gridwidth = GridBagConstraints.REMAINDER;
180         constraints.anchor = GridBagConstraints.WEST;
181         content.add(this.regexSwitch, constraints);
182
183         JPanel regexPanel = new JPanel();
184         regexPanel.setBorder(BorderFactory.createTitledBorder(""));
185         regexPanel.setLayout(new GridLayout(2, 1));
186         regexPanel.add(this.dotallSwitch);
187         regexPanel.add(this.multilineSwitch);
188
189         constraints.gridwidth = GridBagConstraints.REMAINDER;
190         constraints.anchor = GridBagConstraints.WEST;
191         content.add(regexPanel, constraints);
192
193         constraints.weightx = 1.0;
194         constraints.gridwidth = GridBagConstraints.REMAINDER;
195         constraints.fill = GridBagConstraints.HORIZONTAL;
196         content.add(new JSeparator(), constraints);
197
198         constraints.weightx = 0.0;
199         constraints.gridwidth = GridBagConstraints.REMAINDER;
200         constraints.anchor = GridBagConstraints.WEST;
201         constraints.fill = GridBagConstraints.NONE;
202         content.add(this.bulkSearchSwitch, constraints);
203
204         constraints.weightx = 1.0;
205         constraints.gridwidth = GridBagConstraints.REMAINDER;
206         constraints.fill = GridBagConstraints.HORIZONTAL;
207         content.add(new JSeparator(), constraints);
208
209         constraints.weightx = 1.0;
210         constraints.gridwidth = GridBagConstraints.REMAINDER;
211         constraints.anchor = GridBagConstraints.EAST;
212         constraints.fill = GridBagConstraints.NONE;
213         content.add(this.closeButton, constraints);
214
215         return;
216     }
217
218     /**
219      * {@inheritDoc}
220      * 検索ダイアログを表示・非表示する。
221      * @param show 表示フラグ。真なら表示。{@inheritDoc}
222      */
223     @Override
224     public void setVisible(boolean show){
225         super.setVisible(show);
226         getRootPane().setDefaultButton(this.searchButton);
227         this.findBox.requestFocusInWindow();
228         return;
229     }
230
231     /**
232      * ダイアログが閉じられた原因を判定する。
233      * @return キャンセルもしくはクローズボタンでダイアログが閉じられたらtrue
234      */
235     public boolean isCanceled(){
236         return this.canceled;
237     }
238
239     /**
240      * 一括検索が指定されたか否か返す。
241      * @return 一括検索が指定されたらtrue
242      */
243     public boolean isBulkSearch(){
244         return this.bulkSearchSwitch.isSelected();
245     }
246
247     /**
248      * キャンセルボタン押下処理。
249      * このモーダルダイアログを閉じる。
250      */
251     private void actionCancel(){
252         this.canceled = true;
253         setVisible(false);
254         dispose();
255         return;
256     }
257
258     /**
259      * 検索ボタン押下処理。
260      * このモーダルダイアログを閉じる。
261      */
262     private void actionSubmit(){
263         Object selected = this.findBox.getSelectedItem();
264         if(selected == null){
265             this.regexPattern = null;
266             return;
267         }
268         String edit = selected.toString();
269
270         boolean isRegex = this.regexSwitch.isSelected();
271
272         int flag = 0x00000000;
273         if( ! this.capitalSwitch.isSelected() ){
274             flag |= RegexPattern.IGNORECASEFLAG;
275         }
276         if(this.dotallSwitch.isSelected())    flag |= Pattern.DOTALL;
277         if(this.multilineSwitch.isSelected()) flag |= Pattern.MULTILINE;
278
279         try{
280             this.regexPattern = new RegexPattern(edit, isRegex, flag);
281         }catch(PatternSyntaxException e){
282             this.regexPattern = null;
283             if(showRegexError(e)){
284                 return;
285             }
286             actionCancel();
287             return;
288         }
289
290         this.model.addHistory(this.regexPattern);
291
292         this.canceled = false;
293         setVisible(false);
294         dispose();
295
296         return;
297     }
298
299     /**
300      * 正規表現パターン異常系のダイアログ表示。
301      * @param e 正規表現構文エラー
302      * @return 再入力が押されたらtrue。それ以外はfalse。
303      */
304     private boolean showRegexError(PatternSyntaxException e){
305         String pattern = e.getPattern();
306
307         String position = "";
308         int index = e.getIndex();
309         if(0 <= index && index <= pattern.length() - 1){
310             char errChar = pattern.charAt(index);
311             position = "エラーの発生箇所は、おおよそ"
312                       + (index+1) + "文字目 [ " + errChar + " ] "
313                       +"かその前後と推測されます。\n";
314         }
315
316         String message =
317                 "入力された検索文字列 [ " + pattern + " ] は"
318                 +"正しい正規表現として認識されませんでした。\n"
319                 +position
320                 +"正規表現の書き方は"
321                 +" [ http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/"
322                 +"java/util/regex/Pattern.html#sum ] "
323                 +"を参照してください。\n"
324                 +"ただの文字列を検索したい場合は"
325                 +"「正規表現」のチェックボックスを外しましょう。\n"
326                 ;
327
328         Object[] buttons = new Object[2];
329         buttons[0] = LABEL_REENTER;
330         buttons[1] = LABEL_IGNORE;
331         Icon icon = null;
332
333         int optionNo = JOptionPane.showOptionDialog(this,
334                                                     message,
335                                                     "不正な正規表現",
336                                                     JOptionPane.YES_NO_OPTION,
337                                                     JOptionPane.ERROR_MESSAGE,
338                                                     icon,
339                                                     buttons,
340                                                     LABEL_REENTER);
341
342         if(optionNo == JOptionPane.CLOSED_OPTION) return false;
343         if(buttons[optionNo].equals(LABEL_REENTER)) return true;
344         if(buttons[optionNo].equals(LABEL_IGNORE) ) return false;
345
346         return true;
347     }
348
349     /**
350      * 現時点での検索パターンを得る。
351      * @return 検索パターン
352      */
353     public RegexPattern getRegexPattern(){
354         return this.regexPattern;
355     }
356
357     /**
358      * 検索パターンを設定する。
359      * @param pattern 検索パターン
360      */
361     public final void setRegexPattern(RegexPattern pattern){
362         if(pattern == null) this.regexPattern = CustomModel.INITITEM;
363         else                this.regexPattern = pattern;
364
365         String edit = this.regexPattern.getEditSource();
366         this.findBox.getEditor().setItem(edit);
367
368         this.regexSwitch.setSelected(this.regexPattern.isRegex());
369
370         int initflag = this.regexPattern.getRegexFlag();
371         this.capitalSwitch.setSelected(
372                 (initflag & RegexPattern.IGNORECASEFLAG) == 0);
373         this.dotallSwitch.setSelected((initflag & Pattern.DOTALL) != 0);
374         this.multilineSwitch.setSelected((initflag & Pattern.MULTILINE) != 0);
375
376         maskRegexUI();
377
378         return;
379     }
380
381     /**
382      * {@inheritDoc}
383      * ボタン操作時にリスナとして呼ばれる。
384      * @param event イベント {@inheritDoc}
385      */
386     @Override
387     public void actionPerformed(ActionEvent event){
388         Object source = event.getSource();
389         if(source == this.closeButton){
390             actionCancel();
391         }else if(source == this.searchButton){
392             actionSubmit();
393         }else if(source == this.clearButton){
394             this.findBox.getEditor().setItem("");
395             this.findBox.requestFocusInWindow();
396         }
397         return;
398     }
399
400     /**
401      * {@inheritDoc}
402      * コンボボックスのアイテム選択リスナ。
403      * @param event アイテム選択イベント {@inheritDoc}
404      */
405     @Override
406     public void itemStateChanged(ItemEvent event){
407         int stateChange = event.getStateChange();
408         if(stateChange != ItemEvent.SELECTED) return;
409
410         Object item = event.getItem();
411         if( ! (item instanceof RegexPattern) ) return;
412         RegexPattern regex = (RegexPattern) item;
413
414         setRegexPattern(regex);
415
416         return;
417     }
418
419     /**
420      * {@inheritDoc}
421      * チェックボックス操作のリスナ。
422      * @param event チェックボックス操作イベント {@inheritDoc}
423      */
424     @Override
425     public void stateChanged(ChangeEvent event){
426         if(event.getSource() != this.regexSwitch) return;
427         maskRegexUI();
428         return;
429     }
430
431     /**
432      * 正規表現でしか使わないUIのマスク処理。
433      */
434     private void maskRegexUI(){
435         boolean isRegex = this.regexSwitch.isSelected();
436         this.dotallSwitch   .setEnabled(isRegex);
437         this.multilineSwitch.setEnabled(isRegex);
438         return;
439     }
440
441     /**
442      * {@inheritDoc}
443      * コンボボックスのUI変更通知を受け取るリスナ。
444      * @param event UI差し替えイベント {@inheritDoc}
445      */
446     @Override
447     public void propertyChange(PropertyChangeEvent event){
448         if( ! event.getPropertyName().equals("UI") ) return;
449         if(event.getSource() != this.findBox) return;
450
451         ComboBoxEditor editor = this.findBox.getEditor();
452         modifyComboBoxEditor(editor);
453
454         return;
455     }
456
457     /**
458      * コンボボックスエディタを修飾する。
459      * マージン修飾と等幅フォントをいじる。
460      * @param editor エディタ
461      */
462     private void modifyComboBoxEditor(ComboBoxEditor editor){
463         if(editor == null) return;
464
465         Component editComp = editor.getEditorComponent();
466         if(editComp == null) return;
467
468         if(editComp instanceof JTextComponent){
469             JTextComponent textEditor = (JTextComponent) editComp;
470             textEditor.setComponentPopupMenu(new TextPopup());
471         }
472
473         GUIUtils.addMargin(editComp, 1, 4, 1, 4);
474
475         return;
476     }
477
478     /**
479      * 検索履歴をロードする。
480      */
481     public void loadHistory(){
482         JsValue value = ConfigFile.loadJson(new File(HIST_FILE));
483         if(value == null) return;
484
485         if( ! (value instanceof JsObject) ) return;
486         JsObject root = (JsObject) value;
487
488         value = root.getValue("history");
489         if( ! (value instanceof JsArray) ) return;
490         JsArray array = (JsArray) value;
491
492         for(JsValue elem : array){
493             if( ! (elem instanceof JsObject) ) continue;
494             JsObject regObj = (JsObject) elem;
495             RegexPattern regex = RegexPattern.decodeJson(regObj);
496             if(regex == null) continue;
497             this.model.addHistory(regex);
498         }
499
500         this.loadedHistory = root;
501
502         return;
503     }
504
505     /**
506      * 検索履歴をセーブする。
507      */
508     public void saveHistory(){
509         AppSetting setting = Jindolf.getAppSetting();
510         if( ! setting.useConfigPath() ) return;
511         File configPath = setting.getConfigPath();
512         if(configPath == null) return;
513
514         JsObject root = new JsObject();
515         JsArray array = new JsArray();
516         root.putValue("history", array);
517
518         List<RegexPattern> history = this.model.getOriginalHistoryList();
519         history = new ArrayList<RegexPattern>(history);
520         Collections.reverse(history);
521         for(RegexPattern regex : history){
522             JsObject obj = RegexPattern.encodeJson(regex);
523             array.add(obj);
524         }
525
526         if(this.loadedHistory != null){
527             if(this.loadedHistory.equals(root)) return;
528         }
529
530         ConfigFile.saveJson(new File(HIST_FILE), root);
531
532         return;
533     }
534
535     /**
536      * コンボボックスの独自レンダラ。
537      */
538     private static class CustomRenderer extends DefaultListCellRenderer{
539
540         /**
541          * コンストラクタ。
542          */
543         public CustomRenderer(){
544             super();
545             return;
546         }
547
548         /**
549          * {@inheritDoc}
550          * @param list {@inheritDoc}
551          * @param value {@inheritDoc}
552          * @param index {@inheritDoc}
553          * @param isSelected {@inheritDoc}
554          * @param cellHasFocus {@inheritDoc}
555          * @return {@inheritDoc}
556          */
557         @Override
558         public Component getListCellRendererComponent(
559                 JList list,
560                 Object value,
561                 int index,
562                 boolean isSelected,
563                 boolean cellHasFocus ){
564             if(value instanceof JSeparator){
565                 return (JSeparator) value;
566             }
567
568             JLabel superLabel =
569                     (JLabel) super.getListCellRendererComponent(list,
570                                                                 value,
571                                                                 index,
572                                                                 isSelected,
573                                                                 cellHasFocus);
574
575             if(value instanceof RegexPattern){
576                 RegexPattern regexPattern = (RegexPattern) value;
577                 String text;
578                 if(regexPattern.isRegex()){
579                     text = "[R] " + regexPattern.getEditSource();
580                 }else{
581                     text = regexPattern.getEditSource();
582                 }
583                 text += regexPattern.getComment();
584
585                 superLabel.setText(text);
586             }
587
588             GUIUtils.addMargin(superLabel, 1, 4, 1, 4);
589
590             return superLabel;
591         }
592     }
593
594     /**
595      * コンボボックスの独自データモデル。
596      */
597     private static class CustomModel implements ComboBoxModel{
598
599         private static final int HISTORY_MAX = 7;
600         private static final RegexPattern INITITEM =
601             new RegexPattern(
602                 "", false, RegexPattern.IGNORECASEFLAG | Pattern.DOTALL);
603         private static final List<RegexPattern> PREDEF_PATTERN_LIST =
604                 new LinkedList<RegexPattern>();
605
606         static{
607             PREDEF_PATTERN_LIST.add(
608                     new RegexPattern("【[^】]*】",
609                                      true,
610                                      Pattern.DOTALL,
611                                      "     ※ 重要事項") );
612             PREDEF_PATTERN_LIST.add(
613                     new RegexPattern("[■●▼★□○▽☆〇◯∇]",
614                                      true,
615                                      Pattern.DOTALL,
616                                      "     ※ 議題") );
617             PREDEF_PATTERN_LIST.add(
618                     new RegexPattern("Jindolf",
619                                      false,
620                                      RegexPattern.IGNORECASEFLAG,
621                                      "     ※ 宣伝") );
622         }
623
624         private final List<RegexPattern> history =
625                 new LinkedList<RegexPattern>();
626         private final JSeparator separator1st = new JSeparator();
627         private final JSeparator separator2nd = new JSeparator();
628         private Object selected;
629         private final EventListenerList listenerList =
630                 new EventListenerList();
631
632         /**
633          * コンストラクタ。
634          */
635         public CustomModel(){
636             super();
637             return;
638         }
639
640         /**
641          * {@inheritDoc}
642          * @return {@inheritDoc}
643          */
644         @Override
645         public Object getSelectedItem(){
646             return this.selected;
647         }
648
649         /**
650          * {@inheritDoc}
651          * @param item {@inheritDoc}
652          */
653         @Override
654         public void setSelectedItem(Object item){
655             if(item instanceof JSeparator) return;
656             this.selected = item;
657             return;
658         }
659
660         /**
661          * {@inheritDoc}
662          * @param index {@inheritDoc}
663          * @return {@inheritDoc}
664          */
665         @Override
666         public Object getElementAt(int index){
667             int historySize = this.history.size();
668
669             if(index == 0){
670                 return INITITEM;
671             }
672             if(index == 1){
673                 return this.separator1st;
674             }
675             if(2 <= index && index <= 1 + historySize){
676                 return this.history.get(index - 2);
677             }
678             if(index == historySize + 2){
679                 return this.separator2nd;
680             }
681             if(historySize + 3 <= index){
682                 return PREDEF_PATTERN_LIST.get(index - 1
683                                                      - 1
684                                                      - historySize
685                                                      - 1 );
686             }
687
688             return null;
689         }
690
691         /**
692          * {@inheritDoc}
693          * @return {@inheritDoc}
694          */
695         @Override
696         public int getSize(){
697             int size = 1;
698             size += 1;         // first separator
699             size += this.history.size();
700             size += 1;         // second separator
701             size += PREDEF_PATTERN_LIST.size();
702             return size;
703         }
704
705         /**
706          * {@inheritDoc}
707          * @param listener {@inheritDoc}
708          */
709         @Override
710         public void addListDataListener(ListDataListener listener){
711             this.listenerList.add(ListDataListener.class, listener);
712             return;
713         }
714
715         /**
716          * {@inheritDoc}
717          * @param listener {@inheritDoc}
718          */
719         @Override
720         public void removeListDataListener(ListDataListener listener){
721             this.listenerList.remove(ListDataListener.class, listener);
722             return;
723         }
724
725         /**
726          * 検索履歴ヒストリ追加。
727          * @param regexPattern 検索履歴
728          */
729         public void addHistory(RegexPattern regexPattern){
730             if(regexPattern == null) return;
731             if(regexPattern.equals(INITITEM)) return;
732             if(PREDEF_PATTERN_LIST.contains(regexPattern)) return;
733             if(this.history.contains(regexPattern)){
734                 this.history.remove(regexPattern);
735             }
736
737             this.history.add(0, regexPattern);
738
739             while(this.history.size() > HISTORY_MAX){
740                 this.history.remove(HISTORY_MAX);
741             }
742
743             fire();
744
745             return;
746         }
747
748         /**
749          * プリセットでない検索ヒストリリストを返す。
750          * @return 検索ヒストリリスト
751          */
752         public List<RegexPattern> getOriginalHistoryList(){
753             return Collections.unmodifiableList(this.history);
754         }
755
756         /**
757          * ヒストリ追加イベント発火。
758          */
759         private void fire(){
760             ListDataEvent event =
761                     new ListDataEvent(this,
762                                       ListDataEvent.CONTENTS_CHANGED,
763                                       0, getSize() - 1 );
764             ListDataListener[] listeners =
765                     this.listenerList.getListeners(ListDataListener.class);
766             for(ListDataListener listener : listeners){
767                 listener.contentsChanged(event);
768             }
769             return;
770         }
771     }
772
773     // TODO ブックマーク機能との統合
774 }