4 * Copyright(c) 2008 olyutorskii
\r
5 * $Id: FindPanel.java 956 2009-12-13 15:14:07Z olyutorskii $
\r
8 package jp.sourceforge.jindolf;
\r
10 import java.awt.BorderLayout;
\r
11 import java.awt.Component;
\r
12 import java.awt.Container;
\r
13 import java.awt.Frame;
\r
14 import java.awt.GridBagConstraints;
\r
15 import java.awt.GridBagLayout;
\r
16 import java.awt.GridLayout;
\r
17 import java.awt.Insets;
\r
18 import java.awt.event.ActionEvent;
\r
19 import java.awt.event.ActionListener;
\r
20 import java.awt.event.ItemEvent;
\r
21 import java.awt.event.ItemListener;
\r
22 import java.awt.event.WindowAdapter;
\r
23 import java.awt.event.WindowEvent;
\r
24 import java.beans.PropertyChangeEvent;
\r
25 import java.beans.PropertyChangeListener;
\r
26 import java.io.File;
\r
27 import java.util.ArrayList;
\r
28 import java.util.Collections;
\r
29 import java.util.LinkedList;
\r
30 import java.util.List;
\r
31 import java.util.regex.Pattern;
\r
32 import java.util.regex.PatternSyntaxException;
\r
33 import javax.swing.BorderFactory;
\r
34 import javax.swing.ComboBoxEditor;
\r
35 import javax.swing.ComboBoxModel;
\r
36 import javax.swing.DefaultListCellRenderer;
\r
37 import javax.swing.Icon;
\r
38 import javax.swing.JButton;
\r
39 import javax.swing.JCheckBox;
\r
40 import javax.swing.JComboBox;
\r
41 import javax.swing.JDialog;
\r
42 import javax.swing.JLabel;
\r
43 import javax.swing.JList;
\r
44 import javax.swing.JOptionPane;
\r
45 import javax.swing.JPanel;
\r
46 import javax.swing.JSeparator;
\r
47 import javax.swing.border.Border;
\r
48 import javax.swing.event.ChangeEvent;
\r
49 import javax.swing.event.ChangeListener;
\r
50 import javax.swing.event.EventListenerList;
\r
51 import javax.swing.event.ListDataEvent;
\r
52 import javax.swing.event.ListDataListener;
\r
53 import javax.swing.text.JTextComponent;
\r
54 import jp.sourceforge.jindolf.json.JsArray;
\r
55 import jp.sourceforge.jindolf.json.JsObject;
\r
56 import jp.sourceforge.jindolf.json.JsValue;
\r
61 @SuppressWarnings("serial")
\r
62 public class FindPanel extends JDialog
\r
63 implements ActionListener,
\r
66 PropertyChangeListener {
\r
68 private static final String HIST_FILE = "searchHistory.json";
\r
69 private static final String FRAMETITLE = "発言検索 - " + Jindolf.TITLE;
\r
70 private static final String LABEL_REENTER = "再入力";
\r
71 private static final String LABEL_IGNORE = "無視して検索をキャンセル";
\r
73 private final JComboBox findBox = new JComboBox();
\r
74 private final JButton searchButton = new JButton("検索");
\r
75 private final JButton clearButton = new JButton("クリア");
\r
76 private final JCheckBox capitalSwitch =
\r
77 new JCheckBox("大文字/小文字を区別する");
\r
78 private final JCheckBox regexSwitch =
\r
79 new JCheckBox("正規表現");
\r
80 private final JCheckBox dotallSwitch =
\r
81 new JCheckBox("正規表現 \".\" を行末記号にもマッチさせる");
\r
82 private final JCheckBox multilineSwitch =
\r
83 new JCheckBox("正規表現 \"^\" や \"$\" を"
\r
85 private final JCheckBox bulkSearchSwitch =
\r
86 new JCheckBox("全日程を一括検索");
\r
87 private final JButton closeButton = new JButton("キャンセル");
\r
89 private final CustomModel model = new CustomModel();
\r
91 private JsObject loadedHistory = null;
\r
93 private boolean canceled = false;
\r
94 private RegexPattern regexPattern = null;
\r
98 * @param owner 親フレーム。
\r
100 public FindPanel(Frame owner){
\r
101 super(owner, FRAMETITLE, true);
\r
103 GUIUtils.modifyWindowAttributes(this, true, false, true);
\r
105 setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
\r
106 addWindowListener(new WindowAdapter(){
\r
108 public void windowClosing(WindowEvent event){
\r
116 this.findBox.setModel(this.model);
\r
117 this.findBox.setToolTipText("検索文字列を入力してください");
\r
118 this.findBox.setEditable(true);
\r
119 this.findBox.setRenderer(new CustomRenderer());
\r
120 this.findBox.setMaximumRowCount(15);
\r
122 ComboBoxEditor editor = this.findBox.getEditor();
\r
123 modifyComboBoxEditor(editor);
\r
124 this.findBox.addPropertyChangeListener("UI", this);
\r
126 this.searchButton.setToolTipText("発言内容を検索する");
\r
127 this.clearButton.setToolTipText("入力をクリアする");
\r
129 this.findBox.addItemListener(this);
\r
130 this.searchButton.addActionListener(this);
\r
131 this.clearButton.addActionListener(this);
\r
132 this.regexSwitch.addChangeListener(this);
\r
133 this.closeButton.addActionListener(this);
\r
135 setRegexPattern(null);
\r
143 private void design(){
\r
144 Container content = getContentPane();
\r
146 GridBagLayout layout = new GridBagLayout();
\r
147 GridBagConstraints constraints = new GridBagConstraints();
\r
149 content.setLayout(layout);
\r
151 constraints.insets = new Insets(2, 2, 2, 2);
\r
153 constraints.weightx = 1.0;
\r
154 constraints.fill = GridBagConstraints.HORIZONTAL;
\r
155 constraints.gridwidth = 2;
\r
158 .createTitledBorder("検索文字列を入力してください");
\r
159 JPanel panel = new JPanel();
\r
160 panel.setLayout(new BorderLayout());
\r
161 panel.add(this.findBox, BorderLayout.CENTER);
\r
162 panel.setBorder(border);
\r
163 content.add(panel, constraints);
\r
165 constraints.weightx = 0.0;
\r
166 constraints.fill = GridBagConstraints.NONE;
\r
167 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
168 constraints.anchor = GridBagConstraints.SOUTH;
\r
169 content.add(this.searchButton, constraints);
\r
171 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
172 constraints.anchor = GridBagConstraints.WEST;
\r
173 content.add(this.clearButton, constraints);
\r
175 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
176 constraints.anchor = GridBagConstraints.WEST;
\r
177 content.add(this.capitalSwitch, constraints);
\r
179 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
180 constraints.anchor = GridBagConstraints.WEST;
\r
181 content.add(this.regexSwitch, constraints);
\r
183 JPanel regexPanel = new JPanel();
\r
184 regexPanel.setBorder(BorderFactory.createTitledBorder(""));
\r
185 regexPanel.setLayout(new GridLayout(2, 1));
\r
186 regexPanel.add(this.dotallSwitch);
\r
187 regexPanel.add(this.multilineSwitch);
\r
189 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
190 constraints.anchor = GridBagConstraints.WEST;
\r
191 content.add(regexPanel, constraints);
\r
193 constraints.weightx = 1.0;
\r
194 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
195 constraints.fill = GridBagConstraints.HORIZONTAL;
\r
196 content.add(new JSeparator(), constraints);
\r
198 constraints.weightx = 0.0;
\r
199 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
200 constraints.anchor = GridBagConstraints.WEST;
\r
201 constraints.fill = GridBagConstraints.NONE;
\r
202 content.add(this.bulkSearchSwitch, constraints);
\r
204 constraints.weightx = 1.0;
\r
205 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
206 constraints.fill = GridBagConstraints.HORIZONTAL;
\r
207 content.add(new JSeparator(), constraints);
\r
209 constraints.weightx = 1.0;
\r
210 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
211 constraints.anchor = GridBagConstraints.EAST;
\r
212 constraints.fill = GridBagConstraints.NONE;
\r
213 content.add(this.closeButton, constraints);
\r
220 * 検索ダイアログを表示・非表示する。
\r
221 * @param show 表示フラグ。真なら表示。{@inheritDoc}
\r
224 public void setVisible(boolean show){
\r
225 super.setVisible(show);
\r
226 getRootPane().setDefaultButton(this.searchButton);
\r
227 this.findBox.requestFocusInWindow();
\r
232 * ダイアログが閉じられた原因を判定する。
\r
233 * @return キャンセルもしくはクローズボタンでダイアログが閉じられたらtrue
\r
235 public boolean isCanceled(){
\r
236 return this.canceled;
\r
241 * @return 一括検索が指定されたらtrue
\r
243 public boolean isBulkSearch(){
\r
244 return this.bulkSearchSwitch.isSelected();
\r
251 private void actionCancel(){
\r
252 this.canceled = true;
\r
262 private void actionSubmit(){
\r
263 Object selected = this.findBox.getSelectedItem();
\r
264 if(selected == null){
\r
265 this.regexPattern = null;
\r
268 String edit = selected.toString();
\r
270 boolean isRegex = this.regexSwitch.isSelected();
\r
272 int flag = 0x00000000;
\r
273 if( ! this.capitalSwitch.isSelected() ){
\r
274 flag |= RegexPattern.IGNORECASEFLAG;
\r
276 if(this.dotallSwitch.isSelected()) flag |= Pattern.DOTALL;
\r
277 if(this.multilineSwitch.isSelected()) flag |= Pattern.MULTILINE;
\r
280 this.regexPattern = new RegexPattern(edit, isRegex, flag);
\r
281 }catch(PatternSyntaxException e){
\r
282 this.regexPattern = null;
\r
283 if(showRegexError(e)){
\r
290 this.model.addHistory(this.regexPattern);
\r
292 this.canceled = false;
\r
300 * 正規表現パターン異常系のダイアログ表示。
\r
301 * @param e 正規表現構文エラー
\r
302 * @return 再入力が押されたらtrue。それ以外はfalse。
\r
304 private boolean showRegexError(PatternSyntaxException e){
\r
305 String pattern = e.getPattern();
\r
307 String position = "";
\r
308 int index = e.getIndex();
\r
309 if(0 <= index && index <= pattern.length() - 1){
\r
310 char errChar = pattern.charAt(index);
\r
311 position = "エラーの発生箇所は、おおよそ"
\r
312 + (index+1) + "文字目 [ " + errChar + " ] "
\r
313 +"かその前後と推測されます。\n";
\r
317 "入力された検索文字列 [ " + pattern + " ] は"
\r
318 +"正しい正規表現として認識されませんでした。\n"
\r
321 +" [ http://java.sun.com/j2se/1.5.0/ja/docs/ja/api/"
\r
322 +"java/util/regex/Pattern.html#sum ] "
\r
325 +"「正規表現」のチェックボックスを外しましょう。\n"
\r
328 Object[] buttons = new Object[2];
\r
329 buttons[0] = LABEL_REENTER;
\r
330 buttons[1] = LABEL_IGNORE;
\r
333 int optionNo = JOptionPane.showOptionDialog(this,
\r
336 JOptionPane.YES_NO_OPTION,
\r
337 JOptionPane.ERROR_MESSAGE,
\r
342 if(optionNo == JOptionPane.CLOSED_OPTION) return false;
\r
343 if(buttons[optionNo].equals(LABEL_REENTER)) return true;
\r
344 if(buttons[optionNo].equals(LABEL_IGNORE) ) return false;
\r
353 public RegexPattern getRegexPattern(){
\r
354 return this.regexPattern;
\r
359 * @param pattern 検索パターン
\r
361 public final void setRegexPattern(RegexPattern pattern){
\r
362 if(pattern == null) this.regexPattern = CustomModel.INITITEM;
\r
363 else this.regexPattern = pattern;
\r
365 String edit = this.regexPattern.getEditSource();
\r
366 this.findBox.getEditor().setItem(edit);
\r
368 this.regexSwitch.setSelected(this.regexPattern.isRegex());
\r
370 int initflag = this.regexPattern.getRegexFlag();
\r
371 this.capitalSwitch.setSelected(
\r
372 (initflag & RegexPattern.IGNORECASEFLAG) == 0);
\r
373 this.dotallSwitch.setSelected((initflag & Pattern.DOTALL) != 0);
\r
374 this.multilineSwitch.setSelected((initflag & Pattern.MULTILINE) != 0);
\r
383 * ボタン操作時にリスナとして呼ばれる。
\r
384 * @param event イベント {@inheritDoc}
\r
386 public void actionPerformed(ActionEvent event){
\r
387 Object source = event.getSource();
\r
388 if(source == this.closeButton){
\r
390 }else if(source == this.searchButton){
\r
392 }else if(source == this.clearButton){
\r
393 this.findBox.getEditor().setItem("");
\r
394 this.findBox.requestFocusInWindow();
\r
401 * コンボボックスのアイテム選択リスナ。
\r
402 * @param event アイテム選択イベント {@inheritDoc}
\r
404 public void itemStateChanged(ItemEvent event){
\r
405 int stateChange = event.getStateChange();
\r
406 if(stateChange != ItemEvent.SELECTED) return;
\r
408 Object item = event.getItem();
\r
409 if( ! (item instanceof RegexPattern) ) return;
\r
410 RegexPattern regex = (RegexPattern) item;
\r
412 setRegexPattern(regex);
\r
420 * @param event チェックボックス操作イベント {@inheritDoc}
\r
422 public void stateChanged(ChangeEvent event){
\r
423 if(event.getSource() != this.regexSwitch) return;
\r
429 * 正規表現でしか使わないUIのマスク処理。
\r
431 private void maskRegexUI(){
\r
432 boolean isRegex = this.regexSwitch.isSelected();
\r
433 this.dotallSwitch .setEnabled(isRegex);
\r
434 this.multilineSwitch.setEnabled(isRegex);
\r
440 * コンボボックスのUI変更通知を受け取るリスナ。
\r
441 * @param event UI差し替えイベント {@inheritDoc}
\r
443 public void propertyChange(PropertyChangeEvent event){
\r
444 if( ! event.getPropertyName().equals("UI") ) return;
\r
445 if(event.getSource() != this.findBox) return;
\r
447 ComboBoxEditor editor = this.findBox.getEditor();
\r
448 modifyComboBoxEditor(editor);
\r
454 * コンボボックスエディタを修飾する。
\r
455 * マージン修飾と等幅フォントをいじる。
\r
456 * @param editor エディタ
\r
458 private void modifyComboBoxEditor(ComboBoxEditor editor){
\r
459 if(editor == null) return;
\r
461 Component editComp = editor.getEditorComponent();
\r
462 if(editComp == null) return;
\r
464 if(editComp instanceof JTextComponent){
\r
465 JTextComponent textEditor = (JTextComponent) editComp;
\r
466 textEditor.setComponentPopupMenu(new TextPopup());
\r
469 GUIUtils.addMargin(editComp, 1, 4, 1, 4);
\r
477 public void loadHistory(){
\r
478 JsValue value = ConfigFile.loadJson(new File(HIST_FILE));
\r
479 if(value == null) return;
\r
481 if( ! (value instanceof JsObject) ) return;
\r
482 JsObject root = (JsObject) value;
\r
484 value = root.getValue("history");
\r
485 if( ! (value instanceof JsArray) ) return;
\r
486 JsArray array = (JsArray) value;
\r
488 for(JsValue elem : array){
\r
489 if( ! (elem instanceof JsObject) ) continue;
\r
490 JsObject regObj = (JsObject) elem;
\r
491 RegexPattern regex = RegexPattern.decodeJson(regObj);
\r
492 if(regex == null) continue;
\r
493 this.model.addHistory(regex);
\r
496 this.loadedHistory = root;
\r
504 public void saveHistory(){
\r
505 AppSetting setting = Jindolf.getAppSetting();
\r
506 if( ! setting.useConfigPath() ) return;
\r
507 File configPath = setting.getConfigPath();
\r
508 if(configPath == null) return;
\r
510 JsObject root = new JsObject();
\r
511 JsArray array = new JsArray();
\r
512 root.putValue("history", array);
\r
514 List<RegexPattern> history = this.model.getOriginalHistoryList();
\r
515 history = new ArrayList<RegexPattern>(history);
\r
516 Collections.reverse(history);
\r
517 for(RegexPattern regex : history){
\r
518 JsObject obj = RegexPattern.encodeJson(regex);
\r
522 if(this.loadedHistory != null){
\r
523 if(this.loadedHistory.equals(root)) return;
\r
526 ConfigFile.saveJson(new File(HIST_FILE), root);
\r
534 private static class CustomRenderer extends DefaultListCellRenderer{
\r
539 public CustomRenderer(){
\r
546 * @param list {@inheritDoc}
\r
547 * @param value {@inheritDoc}
\r
548 * @param index {@inheritDoc}
\r
549 * @param isSelected {@inheritDoc}
\r
550 * @param cellHasFocus {@inheritDoc}
\r
551 * @return {@inheritDoc}
\r
554 public Component getListCellRendererComponent(
\r
558 boolean isSelected,
\r
559 boolean cellHasFocus ){
\r
560 if(value instanceof JSeparator){
\r
561 return (JSeparator) value;
\r
564 JLabel superLabel =
\r
565 (JLabel) super.getListCellRendererComponent(list,
\r
571 if(value instanceof RegexPattern){
\r
572 RegexPattern regexPattern = (RegexPattern) value;
\r
574 if(regexPattern.isRegex()){
\r
575 text = "[R] " + regexPattern.getEditSource();
\r
577 text = regexPattern.getEditSource();
\r
579 text += regexPattern.getComment();
\r
581 superLabel.setText(text);
\r
584 GUIUtils.addMargin(superLabel, 1, 4, 1, 4);
\r
591 * コンボボックスの独自データモデル。
\r
593 private static class CustomModel implements ComboBoxModel{
\r
595 private static final int HISTORY_MAX = 7;
\r
596 private static final RegexPattern INITITEM =
\r
598 "", false, RegexPattern.IGNORECASEFLAG | Pattern.DOTALL);
\r
599 private static final List<RegexPattern> PREDEF_PATTERN_LIST =
\r
600 new LinkedList<RegexPattern>();
\r
603 PREDEF_PATTERN_LIST.add(
\r
604 new RegexPattern("【[^】]*】",
\r
608 PREDEF_PATTERN_LIST.add(
\r
609 new RegexPattern("[■●▼★□○▽☆〇◯∇]",
\r
613 PREDEF_PATTERN_LIST.add(
\r
614 new RegexPattern("Jindolf",
\r
616 RegexPattern.IGNORECASEFLAG,
\r
620 private final List<RegexPattern> history =
\r
621 new LinkedList<RegexPattern>();
\r
622 private final JSeparator separator1st = new JSeparator();
\r
623 private final JSeparator separator2nd = new JSeparator();
\r
624 private Object selected;
\r
625 private final EventListenerList listenerList =
\r
626 new EventListenerList();
\r
631 public CustomModel(){
\r
638 * @return {@inheritDoc}
\r
640 public Object getSelectedItem(){
\r
641 return this.selected;
\r
646 * @param item {@inheritDoc}
\r
648 public void setSelectedItem(Object item){
\r
649 if(item instanceof JSeparator) return;
\r
650 this.selected = item;
\r
656 * @param index {@inheritDoc}
\r
657 * @return {@inheritDoc}
\r
659 public Object getElementAt(int index){
\r
660 int historySize = this.history.size();
\r
666 return this.separator1st;
\r
668 if(2 <= index && index <= 1 + historySize){
\r
669 return this.history.get(index - 2);
\r
671 if(index == historySize + 2){
\r
672 return this.separator2nd;
\r
674 if(historySize + 3 <= index){
\r
675 return PREDEF_PATTERN_LIST.get(index - 1
\r
686 * @return {@inheritDoc}
\r
688 public int getSize(){
\r
690 size += 1; // first separator
\r
691 size += this.history.size();
\r
692 size += 1; // second separator
\r
693 size += PREDEF_PATTERN_LIST.size();
\r
699 * @param listener {@inheritDoc}
\r
701 public void addListDataListener(ListDataListener listener){
\r
702 this.listenerList.add(ListDataListener.class, listener);
\r
708 * @param listener {@inheritDoc}
\r
710 public void removeListDataListener(ListDataListener listener){
\r
711 this.listenerList.remove(ListDataListener.class, listener);
\r
717 * @param regexPattern 検索履歴
\r
719 public void addHistory(RegexPattern regexPattern){
\r
720 if(regexPattern == null) return;
\r
721 if(regexPattern.equals(INITITEM)) return;
\r
722 if(PREDEF_PATTERN_LIST.contains(regexPattern)) return;
\r
723 if(this.history.contains(regexPattern)){
\r
724 this.history.remove(regexPattern);
\r
727 this.history.add(0, regexPattern);
\r
729 while(this.history.size() > HISTORY_MAX){
\r
730 this.history.remove(HISTORY_MAX);
\r
739 * プリセットでない検索ヒストリリストを返す。
\r
740 * @return 検索ヒストリリスト
\r
742 public List<RegexPattern> getOriginalHistoryList(){
\r
743 return Collections.unmodifiableList(this.history);
\r
749 private void fire(){
\r
750 ListDataEvent event =
\r
751 new ListDataEvent(this,
\r
752 ListDataEvent.CONTENTS_CHANGED,
\r
753 0, getSize() - 1 );
\r
754 ListDataListener[] listeners =
\r
755 this.listenerList.getListeners(ListDataListener.class);
\r
756 for(ListDataListener listener : listeners){
\r
757 listener.contentsChanged(event);
\r
763 // TODO ブックマーク機能との統合
\r