OSDN Git Service

Merge branch 'Branch_release-'
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / editor / TextEditor.java
1 /*
2  * 原稿作成支援用テキストコンポーネント
3  *
4  * License : The MIT License
5  * Copyright(c) 2008 olyutorskii
6  */
7
8 package jp.sfjp.jindolf.editor;
9
10 import java.awt.Rectangle;
11 import java.awt.event.InputMethodEvent;
12 import java.awt.event.InputMethodListener;
13 import java.nio.CharBuffer;
14 import java.text.AttributedCharacterIterator;
15 import javax.swing.JTextArea;
16 import javax.swing.text.AbstractDocument;
17 import javax.swing.text.AttributeSet;
18 import javax.swing.text.BadLocationException;
19 import javax.swing.text.Document;
20 import javax.swing.text.DocumentFilter;
21 import javax.swing.text.DocumentFilter.FilterBypass;
22 import javax.swing.text.PlainDocument;
23
24 /**
25  * 原稿作成支援用テキストコンポーネント。
26  */
27 @SuppressWarnings("serial")
28 public class TextEditor extends JTextArea
29         implements InputMethodListener {
30
31     private static final int MAX_DOCUMENT = 10 * 1000;
32
33     private final DocumentFilter documentFilter = new CustomFilter();
34
35     private boolean onIMEoperation = false;
36
37     /**
38      * コンストラクタ。
39      */
40     @SuppressWarnings("LeakingThisInConstructor")
41     public TextEditor(){
42         super();
43
44         setLineWrap(true);
45         setWrapStyleWord(false);
46
47         Document document = new PlainDocument();
48         setDocument(document);
49
50         addInputMethodListener(this);
51
52         return;
53     }
54
55     /**
56      * エディタが現在IME操作中か判定する。
57      * @return IME操作中ならtrue
58      */
59     public boolean onIMEoperation(){
60         return this.onIMEoperation;
61     }
62
63     /**
64      * 現在のカーソルが表示されるようスクロールエリアを操作する。
65      */
66     public void scrollCaretToVisible(){
67         int caretPosition = getCaretPosition();
68
69         Rectangle caretBounds;
70         try{
71             caretBounds = modelToView(caretPosition);
72         }catch(BadLocationException e){
73             assert false;
74             return;
75         }
76
77         scrollRectToVisible(caretBounds);
78
79         return;
80     }
81
82     /**
83      * {@inheritDoc}
84      * Document変更をフックしてフィルタを仕込む。
85      * @param document {@inheritDoc}
86      */
87     @Override
88     public final void setDocument(Document document){
89         Document oldDocument = getDocument();
90         if(oldDocument instanceof AbstractDocument){
91             AbstractDocument abstractDocument =
92                     (AbstractDocument) oldDocument;
93             abstractDocument.setDocumentFilter(null);
94         }
95
96         super.setDocument(document);
97
98         if(document instanceof AbstractDocument){
99             AbstractDocument abstractDocument = (AbstractDocument) document;
100             abstractDocument.setDocumentFilter(this.documentFilter);
101         }
102
103         return;
104     }
105
106     /**
107      * {@inheritDoc}
108      * このエディタ中の指定領域が表示されるようスクロールエリアを操作する。
109      * キーボードフォーカスを保持しないときは無視。
110      * @param rect {@inheritDoc}
111      */
112     @Override
113     public void scrollRectToVisible(Rectangle rect){
114         if( ! hasFocus() ) return;
115         super.scrollRectToVisible(rect);
116         return;
117     }
118
119     /**
120      * {@inheritDoc}
121      * @param event {@inheritDoc}
122      */
123     @Override
124     public void caretPositionChanged(InputMethodEvent event){
125         // NOTHING
126         return;
127     }
128
129     /**
130      * {@inheritDoc}
131      * このテキストエディタで現在IMEの変換中か否か判定する処理を含む。
132      * @param event {@inheritDoc}
133      */
134     @Override
135     public void inputMethodTextChanged(InputMethodEvent event){
136         int committed = event.getCommittedCharacterCount();
137         AttributedCharacterIterator aci = event.getText();
138         if(aci == null){
139             this.onIMEoperation = false;
140             return;
141         }
142         int begin = aci.getBeginIndex();
143         int end   = aci.getEndIndex();
144         int span = end - begin;
145
146         if(committed >= span) this.onIMEoperation = false;
147         else                  this.onIMEoperation = true;
148
149         return;
150     }
151
152     /**
153      * 入力文字列に制限を加えるDocumentFilter。
154      * \n,\f 以外の制御文字はタブも含め入力禁止。
155      * U+FFFF はjava.textパッケージで特別扱いなのでこれも入力禁止。
156      * ※ ただしIME操作中は制限なし。
157      */
158     private class CustomFilter extends DocumentFilter{
159
160         /**
161          * コンストラクタ。
162          */
163         public CustomFilter(){
164             super();
165             return;
166         }
167
168         /**
169          * 入力禁止文字の判定。
170          * @param ch 検査対象文字
171          * @return 入力禁止ならfalse。ただしIME操作中は必ずtrue。
172          */
173         private boolean isValid(char ch){
174             if(onIMEoperation()) return true;
175
176             if(ch == '\n') return true;
177 //          if(ch == '\f') return true;
178
179             if(ch == '\uffff')             return false;
180             if(Character.isISOControl(ch)) return false;
181
182 //          if( ! CodeX0208.isValid(ch) ) return false;
183             if(Character.isHighSurrogate(ch)) return false;
184             if(Character.isLowSurrogate(ch) ) return false;
185
186             return true;
187         }
188
189         /**
190          * 与えられた文字列から入力禁止文字を除いた文字列に変換する。
191          * @param input 検査対象文字列
192          * @return 除去済み文字列
193          */
194         private String filter(CharSequence input){
195             if(onIMEoperation()) return input.toString();
196
197             int length = input.length();
198             CharBuffer buf = CharBuffer.allocate(length);
199
200             for(int pos = 0; pos < length; pos++){
201                 char ch = input.charAt(pos);
202                 if(ch == '\u2211') ch = '\u03a3'; // Σ変換
203                 if(ch == '\u00ac') ch = '\uffe2'; // ¬変換
204 //              if(ch ==  0x005c ) ch = '\u00a5'; // バックスラッシュから円へ
205                 if(isValid(ch)) buf.append(ch);
206             }
207
208             buf.flip();
209             return buf.toString();
210         }
211
212         /**
213          * {@inheritDoc}
214          * @param fb {@inheritDoc}
215          * @param offset {@inheritDoc}
216          * @param text {@inheritDoc}
217          * @param attrs {@inheritDoc}
218          * @throws javax.swing.text.BadLocationException {@inheritDoc}
219          */
220         @Override
221         public void insertString(FilterBypass fb,
222                                  int offset,
223                                  String text,
224                                  AttributeSet attrs)
225                                  throws BadLocationException{
226             String filtered = filter(text);
227
228             if( ! onIMEoperation() ){
229                 Document document = fb.getDocument();
230                 int docLength = document.getLength();
231                 int rest = MAX_DOCUMENT - docLength;
232                 if(rest < 0){
233                     return;
234                 }else if(rest < filtered.length()){
235                     filtered = filtered.substring(0, rest);
236                 }
237             }
238
239             fb.insertString(offset, filtered, attrs);
240
241             return;
242         }
243
244         /**
245          *  {@inheritDoc}
246          * @param fb {@inheritDoc}
247          * @param offset {@inheritDoc}
248          * @param length {@inheritDoc}
249          * @param text {@inheritDoc}
250          * @param attrs {@inheritDoc}
251          * @throws javax.swing.text.BadLocationException {@inheritDoc}
252          */
253         @Override
254         public void replace(FilterBypass fb,
255                             int offset,
256                             int length,
257                             String text,
258                             AttributeSet attrs)
259                             throws BadLocationException{
260             String filtered = filter(text);
261
262             if( ! onIMEoperation() ){
263                 Document document = fb.getDocument();
264                 int docLength = document.getLength();
265                 docLength -= length;
266                 int rest = MAX_DOCUMENT - docLength;
267                 if(rest < 0){
268                     return;
269                 }else if(rest < filtered.length()){
270                     filtered = filtered.substring(0, rest);
271                 }
272             }
273
274             fb.replace(offset, length, filtered, attrs);
275
276             return;
277         }
278     }
279
280     // TODO 禁則チェック。20文字を超える長大なブレーク禁止文字列の出現の監視。
281     // TODO 連続したホワイトスペースに対する警告。
282     // TODO 先頭もしくは末尾のホワイトスペース出現に対する警告。
283     // TODO 改行記号の表示
284     // TODO 改発言記号の導入
285 }