OSDN Git Service

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