OSDN Git Service

mainエントリのパッケージを変更。
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / editor / EditArray.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.Dimension;
11 import java.awt.EventQueue;
12 import java.awt.Font;
13 import java.awt.GridBagConstraints;
14 import java.awt.GridBagLayout;
15 import java.awt.LayoutManager;
16 import java.awt.Rectangle;
17 import java.awt.event.FocusEvent;
18 import java.awt.event.FocusListener;
19 import java.util.ArrayList;
20 import java.util.List;
21 import javax.swing.JPanel;
22 import javax.swing.Scrollable;
23 import javax.swing.SwingConstants;
24 import javax.swing.event.ChangeEvent;
25 import javax.swing.event.ChangeListener;
26 import javax.swing.event.DocumentEvent;
27 import javax.swing.event.DocumentListener;
28 import javax.swing.text.BadLocationException;
29 import javax.swing.text.Document;
30 import javax.swing.text.JTextComponent;
31 import javax.swing.text.NavigationFilter;
32 import javax.swing.text.Position.Bias;
33
34 /**
35  * エディタ集合の操作。
36  * ※ このクラスはすべてシングルスレッドモデルで作られている。
37  */
38 @SuppressWarnings("serial")
39 public class EditArray extends JPanel
40                        implements Scrollable,
41                                   FocusListener {
42
43     private static final int MAX_EDITORS = 50;
44
45     private final List<TalkEditor> editorList = new ArrayList<TalkEditor>();
46     private boolean onAdjusting = false;
47
48     private final NavigationFilter keyNavigator = new CustomNavigation();
49     private final DocumentListener documentListener = new DocWatcher();
50
51     private TalkEditor activeEditor;
52
53     private Font textFont;
54
55     /**
56      * コンストラクタ。
57      */
58     public EditArray(){
59         super();
60
61         setOpaque(false);
62
63         LayoutManager layout = new GridBagLayout();
64         setLayout(layout);
65
66         TalkEditor firstEditor = incrementTalkEditor();
67         setActiveEditor(firstEditor);
68
69         return;
70     }
71
72     /**
73      * 個別エディタの生成を行う。
74      * @return エディタ
75      */
76     private TalkEditor createTalkEditor(){
77         TalkEditor editor = new TalkEditor();
78         editor.setNavigationFilter(this.keyNavigator);
79         editor.addTextFocusListener(this);
80         Document document = editor.getDocument();
81         document.addDocumentListener(this.documentListener);
82
83         if(this.textFont == null){
84             this.textFont = editor.getTextFont();
85         }else{
86             editor.setTextFont(this.textFont);
87         }
88
89         return editor;
90     }
91
92     /**
93      * エディタ集合を一つ増やす。
94      * @return 増えたエディタ
95      */
96     private TalkEditor incrementTalkEditor(){
97         TalkEditor editor = createTalkEditor();
98
99         GridBagConstraints constraints = new GridBagConstraints();
100
101         constraints.gridx = 0;
102         constraints.gridy = GridBagConstraints.RELATIVE;
103
104         constraints.gridwidth = GridBagConstraints.REMAINDER;
105         constraints.gridheight = 1;
106
107         constraints.weightx = 1.0;
108         constraints.weighty = 0.0;
109
110         constraints.fill = GridBagConstraints.HORIZONTAL;
111         constraints.anchor = GridBagConstraints.NORTHEAST;
112
113         add(editor, constraints);
114
115         this.editorList.add(editor);
116
117         int sequenceNumber = this.editorList.size();
118         editor.setSequenceNumber(sequenceNumber);
119
120         return editor;
121     }
122
123     /**
124      * 1から始まる通し番号指定でエディタを取得する。
125      * 存在しない通し番号が指定された場合は新たにエディタが追加される。
126      * @param sequenceNumber 通し番号
127      * @return エディタ
128      */
129     private TalkEditor getTalkEditor(int sequenceNumber){
130         while(this.editorList.size() < sequenceNumber){
131             incrementTalkEditor();
132         }
133
134         TalkEditor result = this.editorList.get(sequenceNumber - 1);
135
136         return result;
137     }
138
139     /**
140      * 指定したエディタの次の通し番号を持つエディタを返す。
141      * エディタがなければ追加される。
142      * @param editor エディタ
143      * @return 次のエディタ
144      */
145     private TalkEditor nextEditor(TalkEditor editor){
146         int sequenceNumber = editor.getSequenceNumber();
147         TalkEditor nextEditor = getTalkEditor(sequenceNumber + 1);
148         return nextEditor;
149     }
150
151     /**
152      * 指定したエディタの前の通し番号を持つエディタを返す。
153      * @param editor エディタ
154      * @return 前のエディタ。
155      * 最初のエディタ(通し番号1)が指定されればnullを返す。
156      */
157     private TalkEditor prevEditor(TalkEditor editor){
158         int sequenceNumber = editor.getSequenceNumber();
159         if(sequenceNumber <= 1) return null;
160         TalkEditor prevEditor = getTalkEditor(sequenceNumber - 1);
161         return prevEditor;
162     }
163
164     /**
165      * 指定したエディタがエディタ集合の最後のエディタか判定する。
166      * @param editor エディタ
167      * @return 最後のエディタならtrue
168      */
169     private boolean isLastEditor(TalkEditor editor){
170         int seqNo = editor.getSequenceNumber();
171         int size = this.editorList.size();
172         if(seqNo >= size) return true;
173         return false;
174     }
175
176     /**
177      * Documentからその持ち主であるエディタを取得する。
178      * @param document Documentインスタンス
179      * @return 持ち主のエディタ。見つからなければnull。
180      */
181     private TalkEditor getEditorFromDocument(Document document){
182         for(TalkEditor editor : this.editorList){
183             if(editor.getDocument() == document) return editor;
184         }
185         return null;
186     }
187
188     /**
189      * エディタ集合から任意のエディタを除く。
190      * ただし最初のエディタは消去不可。
191      * @param editor エディタ
192      */
193     private void removeEditor(TalkEditor editor){
194         if(editor.getParent() != this) return;
195
196         int seqNo = editor.getSequenceNumber();
197         if(seqNo <= 1) return;
198         TalkEditor prevEditor = prevEditor(editor);
199         if(editor.isActive()){
200             setActiveEditor(prevEditor);
201         }
202         if(editor.hasEditorFocus()){
203             prevEditor.requestEditorFocus();
204         }
205
206         this.editorList.remove(seqNo - 1);
207
208         editor.setNavigationFilter(null);
209         editor.removeTextFocusListener(this);
210         Document document = editor.getDocument();
211         document.removeDocumentListener(this.documentListener);
212         editor.clearText();
213
214         remove(editor);
215         revalidate();
216
217         int renumber = 1;
218         for(TalkEditor newEditor : this.editorList){
219             newEditor.setSequenceNumber(renumber++);
220         }
221
222         return;
223     }
224
225     /**
226      * エディタ間文字調整タスクをディスパッチスレッドとして事後投入する。
227      * エディタ間文字調整タスクが実行中であれば何もしない。
228      * きっかけとなったエディタ上でIME操作が確定していなければ何もしない。
229      * @param triggerEvent ドキュメント変更イベント
230      */
231     private void detachAdjustTask(DocumentEvent triggerEvent){
232         if(this.onAdjusting) return;
233
234         Document document = triggerEvent.getDocument();
235         final TalkEditor triggerEditor = getEditorFromDocument(document);
236         if(triggerEditor.onIMEoperation()) return;
237
238         this.onAdjusting = true;
239
240         EventQueue.invokeLater(new Runnable(){
241             public void run(){
242                 try{
243                     adjustTask(triggerEditor);
244                 }finally{
245                     EditArray.this.onAdjusting = false;
246                 }
247                 return;
248             }
249         });
250
251         return;
252     }
253
254     /**
255      * エディタ間文字調整タスク本体。
256      * @param triggerEditor タスク実行のきっかけとなったエディタ
257      */
258     private void adjustTask(TalkEditor triggerEditor){
259         int initCaretPos = triggerEditor.getCaretPosition();
260
261         TalkEditor newFocus = null;
262         int newCaretPos = -1;
263
264         TalkEditor current = triggerEditor;
265         for(;;){
266             TalkEditor next;
267
268             if( ! isLastEditor(current) ){
269                 next = nextEditor(current);
270                 String nextContents = next.getText();
271                 int nextLength = nextContents.length();
272
273                 current.appendTail(nextContents);
274                 String rest = current.chopRest();
275                 int restLength;
276                 if(rest == null) restLength = 0;
277                 else             restLength = rest.length();
278
279                 int chopLength = nextLength - restLength;
280                 if(chopLength > 0){
281                     next.chopHead(chopLength);
282                 }else if(chopLength < 0){
283                     rest = rest.substring(0, -chopLength);
284                     next.appendHead(rest);
285                 }else{
286                     if(newFocus == null){
287                         newFocus = current;
288                         newCaretPos = initCaretPos;
289                     }
290                     break;
291                 }
292             }else{
293                 String rest = current.chopRest();
294                 if(rest == null || this.editorList.size() >= MAX_EDITORS){
295                     if(newFocus == null){
296                         newFocus = current;
297                         if(current.getTextLength() >= initCaretPos){
298                             newCaretPos = initCaretPos;
299                         }else{
300                             newCaretPos = current.getTextLength();
301                         }
302                     }
303                     break;
304                 }
305                 next = nextEditor(current);
306                 next.appendHead(rest);
307             }
308
309             if(newFocus == null){
310                 int currentLength = current.getTextLength();
311                 if(initCaretPos >= currentLength){
312                     initCaretPos -= currentLength;
313                 }else{
314                     newFocus = current;
315                     newCaretPos = initCaretPos;
316                 }
317             }
318
319             current = next;
320         }
321
322         if(newFocus != null){
323             newFocus.requestEditorFocus();
324             newFocus.setCaretPosition(newCaretPos);
325         }
326
327         adjustEditorsTail();
328
329         return;
330     }
331
332     /**
333      * エディタ集合末尾の空エディタを切り詰める。
334      * ただし最初のエディタ(通し番号1)は削除されない。
335      * フォーカスを持つエディタが削除された場合は、
336      * 削除されなかった最後のエディタにフォーカスが移る。
337      */
338     private void adjustEditorsTail(){
339         int editorNum = this.editorList.size();
340         if(editorNum <= 0) return;
341         TalkEditor lastEditor = this.editorList.get(editorNum - 1);
342
343         TalkEditor prevlostEditor = null;
344
345         boolean lostFocusedEditor = false;
346
347         for(;;){
348             int textLength = lastEditor.getTextLength();
349             int seqNo = lastEditor.getSequenceNumber();
350
351             if(lostFocusedEditor){
352                 prevlostEditor = lastEditor;
353             }
354
355             if(textLength > 0) break;
356             if(seqNo <= 1) break;
357
358             if(lastEditor.hasEditorFocus()) lostFocusedEditor = true;
359             removeEditor(lastEditor);
360
361             lastEditor = prevEditor(lastEditor); // TODO ちょっと変
362         }
363
364         if(prevlostEditor != null){
365             int textLength = prevlostEditor.getTextLength();
366             prevlostEditor.requestEditorFocus();
367             prevlostEditor.setCaretPosition(textLength);
368         }
369
370         return;
371     }
372
373     /**
374      * フォーカスを持つエディタを取得する。
375      * @return エディタ
376      */
377     public TalkEditor getFocusedTalkEditor(){
378         for(TalkEditor editor : this.editorList){
379             if(editor.hasEditorFocus()) return editor;
380         }
381         return null;
382     }
383
384     /**
385      * フォーカスを持つエディタの次エディタがあればフォーカスを移し、
386      * カレット位置を0にする。
387      */
388     // TODO エディタのスクロール位置調整が必要。
389     public void forwardEditor(){
390         TalkEditor editor = getFocusedTalkEditor();
391         if(isLastEditor(editor)) return;
392         TalkEditor next = nextEditor(editor);
393         next.setCaretPosition(0);
394         next.requestEditorFocus();
395         return;
396     }
397
398     /**
399      * フォーカスを持つエディタの前エディタがあればフォーカスを移し、
400      * カレット位置を末尾に置く。
401      */
402     public void backwardEditor(){
403         TalkEditor editor = getFocusedTalkEditor();
404         TalkEditor prev = prevEditor(editor);
405         if(prev == null) return;
406         int length = prev.getTextLength();
407         prev.setCaretPosition(length);
408         prev.requestEditorFocus();
409         return;
410     }
411
412     /**
413      * 任意のエディタをアクティブにする。
414      * 同時にアクティブなエディタは一つのみ。
415      * @param editor アクティブにするエディタ
416      */
417     private void setActiveEditor(TalkEditor editor){
418         if(this.activeEditor != null){
419             this.activeEditor.setActive(false);
420         }
421
422         this.activeEditor = editor;
423
424         if(this.activeEditor != null){
425             this.activeEditor.setActive(true);
426         }
427
428         fireChangeActive();
429
430         return;
431     }
432
433     /**
434      * アクティブなエディタを返す。
435      * @return アクティブなエディタ。
436      */
437     public TalkEditor getActiveEditor(){
438         return this.activeEditor;
439     }
440
441     /**
442      * 全発言を連結した文字列を返す。
443      * @return 連結文字列
444      */
445     public CharSequence getAllText(){
446         StringBuilder result = new StringBuilder();
447
448         for(TalkEditor editor : this.editorList){
449             String text = editor.getText();
450             result.append(text);
451         }
452
453         return result;
454     }
455
456     /**
457      * 先頭エディタの0文字目から字を詰め込む。
458      * 2番目移行のエディタへはみ出すかもしれない。
459      * @param seq 詰め込む文字列
460      */
461     public void setAllText(CharSequence seq){
462         TalkEditor firstEditor = getTalkEditor(1);
463         Document doc = firstEditor.getDocument();
464         try{
465             doc.insertString(0, seq.toString(), null);
466         }catch(BadLocationException e){
467             assert false;
468         }
469         return;
470     }
471
472     /**
473      * 全エディタをクリアする。
474      */
475     public void clearAllEditor(){
476         int editorNum = this.editorList.size();
477         if(editorNum <= 0) return;
478
479         TalkEditor lastEditor = this.editorList.get(editorNum - 1);
480         for(;;){
481             removeEditor(lastEditor);
482             lastEditor = prevEditor(lastEditor);
483             if(lastEditor == null) break;
484         }
485
486         TalkEditor firstEditor = getTalkEditor(1);
487         firstEditor.clearText();
488         setActiveEditor(firstEditor);
489
490         return;
491     }
492
493     /**
494      * テキスト編集用フォントを指定する。
495      * @param textFont フォント
496      */
497     public void setTextFont(Font textFont){
498         this.textFont = textFont;
499         for(TalkEditor editor : this.editorList){
500             editor.setTextFont(this.textFont);
501             editor.repaint();
502         }
503         revalidate();
504         return;
505     }
506
507     /**
508      * テキスト編集用フォントを取得する。
509      * @return フォント
510      */
511     public Font getTextFont(){
512         return this.textFont;
513     }
514
515     /**
516      * アクティブエディタ変更通知用リスナの登録。
517      * @param listener リスナ
518      */
519     public void addChangeListener(ChangeListener listener){
520         this.listenerList.add(ChangeListener.class, listener);
521         return;
522     }
523
524     /**
525      * アクティブエディタ変更通知用リスナの削除。
526      * @param listener リスナ
527      */
528     public void removeChangeListener(ChangeListener listener){
529         this.listenerList.remove(ChangeListener.class, listener);
530         return;
531     }
532
533     /**
534      * アクティブエディタ変更通知を行う。
535      */
536     private void fireChangeActive(){
537         ChangeEvent event = new ChangeEvent(this);
538
539         ChangeListener[] listeners =
540                 this.listenerList.getListeners(ChangeListener.class);
541         for(ChangeListener listener : listeners){
542             listener.stateChanged(event);
543         }
544
545         return;
546     }
547
548     /**
549      * {@inheritDoc}
550      * エディタのフォーカス取得とともにアクティブ状態にする。
551      * @param event {@inheritDoc}
552      */
553     @Override
554     public void focusGained(FocusEvent event){
555         Object source = event.getSource();
556         if( ! (source instanceof JTextComponent) ) return;
557         JTextComponent textComp = (JTextComponent) source;
558
559         Document document = textComp.getDocument();
560         TalkEditor editor = getEditorFromDocument(document);
561
562         setActiveEditor(editor);
563
564         return;
565     }
566
567     /**
568      * {@inheritDoc}
569      * @param event {@inheritDoc}
570      */
571     @Override
572     public void focusLost(FocusEvent event){
573         // NOTHING
574         return;
575     }
576
577     /**
578      * {@inheritDoc}
579      * @return {@inheritDoc}
580      */
581     @Override
582     public Dimension getPreferredScrollableViewportSize(){
583         Dimension result = getPreferredSize();
584         return result;
585     }
586
587     /**
588      * {@inheritDoc}
589      * 横スクロールバーを極力出さないようレイアウトでがんばる。
590      * @return {@inheritDoc}
591      */
592     @Override
593     public boolean getScrollableTracksViewportWidth(){
594         return true;
595     }
596
597     /**
598      * {@inheritDoc}
599      * 縦スクロールバーを出しても良いのでレイアウトでがんばらない。
600      * @return {@inheritDoc}
601      */
602     @Override
603     public boolean getScrollableTracksViewportHeight(){
604         return false;
605     }
606
607     /**
608      *  {@inheritDoc}
609      * @param visibleRect {@inheritDoc}
610      * @param orientation {@inheritDoc}
611      * @param direction {@inheritDoc}
612      * @return {@inheritDoc}
613      */
614     @Override
615     public int getScrollableBlockIncrement(Rectangle visibleRect,
616                                            int orientation,
617                                            int direction ){
618         if(orientation == SwingConstants.VERTICAL){
619             return visibleRect.height;
620         }
621         return 10;
622     }
623
624     /**
625      * {@inheritDoc}
626      * @param visibleRect {@inheritDoc}
627      * @param orientation {@inheritDoc}
628      * @param direction {@inheritDoc}
629      * @return {@inheritDoc}
630      */
631     @Override
632     public int getScrollableUnitIncrement(Rectangle visibleRect,
633                                           int orientation,
634                                           int direction ){
635         return 30; // TODO フォント高の1.5倍くらい?
636     }
637
638     /**
639      * エディタ内のカーソル移動を監視するための、
640      * カスタム化したナビゲーションフィルター。
641      * 必要に応じてエディタ間カーソル移動を行う。
642      */
643     private class CustomNavigation extends NavigationFilter{
644
645         /**
646          * コンストラクタ。
647          */
648         public CustomNavigation(){
649             super();
650             return;
651         }
652
653         /**
654          * {@inheritDoc}
655          * カーソル移動が行き詰まった場合、
656          * 隣接するエディタ間でカーソル移動を行う。
657          * @param text {@inheritDoc}
658          * @param pos {@inheritDoc}
659          * @param bias {@inheritDoc}
660          * @param direction {@inheritDoc}
661          * @param biasRet {@inheritDoc}
662          * @return {@inheritDoc}
663          * @throws javax.swing.text.BadLocationException {@inheritDoc}
664          */
665         @Override
666         public int getNextVisualPositionFrom(JTextComponent text,
667                                                  int pos,
668                                                  Bias bias,
669                                                  int direction,
670                                                  Bias[] biasRet )
671                                                  throws BadLocationException {
672             int result = super.getNextVisualPositionFrom(text,
673                                                          pos,
674                                                          bias,
675                                                          direction,
676                                                          biasRet );
677             if(result != pos) return result;
678
679             switch(direction){
680             case SwingConstants.WEST:
681             case SwingConstants.NORTH:
682                 backwardEditor();
683                 break;
684             case SwingConstants.EAST:
685             case SwingConstants.SOUTH:
686                 forwardEditor();
687                 break;
688             default:
689                 assert false;
690             }
691
692             return result;
693         }
694     }
695
696     /**
697      * エディタの内容変更を監視し、随時エディタ間調整を行う。
698      */
699     private class DocWatcher implements DocumentListener{
700
701         /**
702          * コンストラクタ。
703          */
704         public DocWatcher(){
705             super();
706             return;
707         }
708
709         /**
710          * {@inheritDoc}
711          * @param event {@inheritDoc}
712          */
713         @Override
714         public void changedUpdate(DocumentEvent event){
715             detachAdjustTask(event);
716             return;
717         }
718
719         /**
720          * {@inheritDoc}
721          * @param event {@inheritDoc}
722          */
723         @Override
724         public void insertUpdate(DocumentEvent event){
725             detachAdjustTask(event);
726             return;
727         }
728
729         /**
730          * {@inheritDoc}
731          * @param event {@inheritDoc}
732          */
733         @Override
734         public void removeUpdate(DocumentEvent event){
735             detachAdjustTask(event);
736             return;
737         }
738     }
739
740 }