4 * Copyright(c) 2008 olyutorskii
\r
5 * $Id: EditArray.java 953 2009-12-06 16:42:14Z olyutorskii $
\r
8 package jp.sourceforge.jindolf;
\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
36 * ※ このクラスはすべてシングルスレッドモデルで作られている。
\r
38 @SuppressWarnings("serial")
\r
39 public class EditArray extends JPanel
\r
40 implements Scrollable,
\r
43 private static final int MAX_EDITORS = 50;
\r
45 private final List<TalkEditor> editorList = new ArrayList<TalkEditor>();
\r
46 private boolean onAdjusting = false;
\r
48 private final NavigationFilter keyNavigator = new CustomNavigation();
\r
49 private final DocumentListener documentListener = new DocWatcher();
\r
51 private TalkEditor activeEditor;
\r
53 private Font textFont;
\r
63 LayoutManager layout = new GridBagLayout();
\r
66 TalkEditor firstEditor = incrementTalkEditor();
\r
67 setActiveEditor(firstEditor);
\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
83 if(this.textFont == null){
\r
84 this.textFont = editor.getTextFont();
\r
86 editor.setTextFont(this.textFont);
\r
96 private TalkEditor incrementTalkEditor(){
\r
97 TalkEditor editor = createTalkEditor();
\r
99 GridBagConstraints constraints = new GridBagConstraints();
\r
101 constraints.gridx = 0;
\r
102 constraints.gridy = GridBagConstraints.RELATIVE;
\r
104 constraints.gridwidth = GridBagConstraints.REMAINDER;
\r
105 constraints.gridheight = 1;
\r
107 constraints.weightx = 1.0;
\r
108 constraints.weighty = 0.0;
\r
110 constraints.fill = GridBagConstraints.HORIZONTAL;
\r
111 constraints.anchor = GridBagConstraints.NORTHEAST;
\r
113 add(editor, constraints);
\r
115 this.editorList.add(editor);
\r
117 int sequenceNumber = this.editorList.size();
\r
118 editor.setSequenceNumber(sequenceNumber);
\r
124 * 1から始まる通し番号指定でエディタを取得する。
\r
125 * 存在しない通し番号が指定された場合は新たにエディタが追加される。
\r
126 * @param sequenceNumber 通し番号
\r
129 private TalkEditor getTalkEditor(int sequenceNumber){
\r
130 while(this.editorList.size() < sequenceNumber){
\r
131 incrementTalkEditor();
\r
134 TalkEditor result = this.editorList.get(sequenceNumber - 1);
\r
140 * 指定したエディタの次の通し番号を持つエディタを返す。
\r
142 * @param editor エディタ
\r
145 private TalkEditor nextEditor(TalkEditor editor){
\r
146 int sequenceNumber = editor.getSequenceNumber();
\r
147 TalkEditor nextEditor = getTalkEditor(sequenceNumber + 1);
\r
152 * 指定したエディタの前の通し番号を持つエディタを返す。
\r
153 * @param editor エディタ
\r
155 * 最初のエディタ(通し番号1)が指定されればnullを返す。
\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
165 * 指定したエディタがエディタ集合の最後のエディタか判定する。
\r
166 * @param editor エディタ
\r
167 * @return 最後のエディタならtrue
\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
177 * Documentからその持ち主であるエディタを取得する。
\r
178 * @param document Documentインスタンス
\r
179 * @return 持ち主のエディタ。見つからなければnull。
\r
181 private TalkEditor getEditorFromDocument(Document document){
\r
182 for(TalkEditor editor : this.editorList){
\r
183 if(editor.getDocument() == document) return editor;
\r
189 * エディタ集合から任意のエディタを除く。
\r
191 * @param editor エディタ
\r
193 private void removeEditor(TalkEditor editor){
\r
194 if(editor.getParent() != this) return;
\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
202 if(editor.hasEditorFocus()){
\r
203 prevEditor.requestEditorFocus();
\r
206 this.editorList.remove(seqNo - 1);
\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
218 for(TalkEditor newEditor : this.editorList){
\r
219 newEditor.setSequenceNumber(renumber++);
\r
226 * エディタ間文字調整タスクをディスパッチスレッドとして事後投入する。
\r
227 * エディタ間文字調整タスクが実行中であれば何もしない。
\r
228 * きっかけとなったエディタ上でIME操作が確定していなければ何もしない。
\r
229 * @param triggerEvent ドキュメント変更イベント
\r
231 private void detachAdjustTask(DocumentEvent triggerEvent){
\r
232 if(this.onAdjusting) return;
\r
234 Document document = triggerEvent.getDocument();
\r
235 final TalkEditor triggerEditor = getEditorFromDocument(document);
\r
236 if(triggerEditor.onIMEoperation()) return;
\r
238 this.onAdjusting = true;
\r
240 EventQueue.invokeLater(new Runnable(){
\r
243 adjustTask(triggerEditor);
\r
245 EditArray.this.onAdjusting = false;
\r
256 * @param triggerEditor タスク実行のきっかけとなったエディタ
\r
258 private void adjustTask(TalkEditor triggerEditor){
\r
259 int initCaretPos = triggerEditor.getCaretPosition();
\r
261 TalkEditor newFocus = null;
\r
262 int newCaretPos = -1;
\r
264 TalkEditor current = triggerEditor;
\r
268 if( ! isLastEditor(current) ){
\r
269 next = nextEditor(current);
\r
270 String nextContents = next.getText();
\r
271 int nextLength = nextContents.length();
\r
273 current.appendTail(nextContents);
\r
274 String rest = current.chopRest();
\r
276 if(rest == null) restLength = 0;
\r
277 else restLength = rest.length();
\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
286 if(newFocus == null){
\r
287 newFocus = current;
\r
288 newCaretPos = initCaretPos;
\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
300 newCaretPos = current.getTextLength();
\r
305 next = nextEditor(current);
\r
306 next.appendHead(rest);
\r
309 if(newFocus == null){
\r
310 int currentLength = current.getTextLength();
\r
311 if(initCaretPos >= currentLength){
\r
312 initCaretPos -= currentLength;
\r
314 newFocus = current;
\r
315 newCaretPos = initCaretPos;
\r
322 if(newFocus != null){
\r
323 newFocus.requestEditorFocus();
\r
324 newFocus.setCaretPosition(newCaretPos);
\r
327 adjustEditorsTail();
\r
333 * エディタ集合末尾の空エディタを切り詰める。
\r
334 * ただし最初のエディタ(通し番号1)は削除されない。
\r
335 * フォーカスを持つエディタが削除された場合は、
\r
336 * 削除されなかった最後のエディタにフォーカスが移る。
\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
343 TalkEditor prevlostEditor = null;
\r
345 boolean lostFocusedEditor = false;
\r
348 int textLength = lastEditor.getTextLength();
\r
349 int seqNo = lastEditor.getSequenceNumber();
\r
351 if(lostFocusedEditor){
\r
352 prevlostEditor = lastEditor;
\r
355 if(textLength > 0) break;
\r
356 if(seqNo <= 1) break;
\r
358 if(lastEditor.hasEditorFocus()) lostFocusedEditor = true;
\r
359 removeEditor(lastEditor);
\r
361 lastEditor = prevEditor(lastEditor); // TODO ちょっと変
\r
364 if(prevlostEditor != null){
\r
365 int textLength = prevlostEditor.getTextLength();
\r
366 prevlostEditor.requestEditorFocus();
\r
367 prevlostEditor.setCaretPosition(textLength);
\r
374 * フォーカスを持つエディタを取得する。
\r
377 public TalkEditor getFocusedTalkEditor(){
\r
378 for(TalkEditor editor : this.editorList){
\r
379 if(editor.hasEditorFocus()) return editor;
\r
385 * フォーカスを持つエディタの次エディタがあればフォーカスを移し、
\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
399 * フォーカスを持つエディタの前エディタがあればフォーカスを移し、
\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
413 * 任意のエディタをアクティブにする。
\r
414 * 同時にアクティブなエディタは一つのみ。
\r
415 * @param editor アクティブにするエディタ
\r
417 private void setActiveEditor(TalkEditor editor){
\r
418 if(this.activeEditor != null){
\r
419 this.activeEditor.setActive(false);
\r
422 this.activeEditor = editor;
\r
424 if(this.activeEditor != null){
\r
425 this.activeEditor.setActive(true);
\r
428 fireChangeActive();
\r
435 * @return アクティブなエディタ。
\r
437 public TalkEditor getActiveEditor(){
\r
438 return this.activeEditor;
\r
445 public CharSequence getAllText(){
\r
446 StringBuilder result = new StringBuilder();
\r
448 for(TalkEditor editor : this.editorList){
\r
449 String text = editor.getText();
\r
450 result.append(text);
\r
457 * 先頭エディタの0文字目から字を詰め込む。
\r
458 * 2番目移行のエディタへはみ出すかもしれない。
\r
459 * @param seq 詰め込む文字列
\r
461 public void setAllText(CharSequence seq){
\r
462 TalkEditor firstEditor = getTalkEditor(1);
\r
463 Document doc = firstEditor.getDocument();
\r
465 doc.insertString(0, seq.toString(), null);
\r
466 }catch(BadLocationException e){
\r
475 public void clearAllEditor(){
\r
476 int editorNum = this.editorList.size();
\r
477 if(editorNum <= 0) return;
\r
479 TalkEditor lastEditor = this.editorList.get(editorNum - 1);
\r
481 removeEditor(lastEditor);
\r
482 lastEditor = prevEditor(lastEditor);
\r
483 if(lastEditor == null) break;
\r
486 TalkEditor firstEditor = getTalkEditor(1);
\r
487 firstEditor.clearText();
\r
488 setActiveEditor(firstEditor);
\r
494 * テキスト編集用フォントを指定する。
\r
495 * @param textFont フォント
\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
508 * テキスト編集用フォントを取得する。
\r
511 public Font getTextFont(){
\r
512 return this.textFont;
\r
516 * アクティブエディタ変更通知用リスナの登録。
\r
517 * @param listener リスナ
\r
519 public void addChangeListener(ChangeListener listener){
\r
520 this.listenerList.add(ChangeListener.class, listener);
\r
525 * アクティブエディタ変更通知用リスナの削除。
\r
526 * @param listener リスナ
\r
528 public void removeChangeListener(ChangeListener listener){
\r
529 this.listenerList.remove(ChangeListener.class, listener);
\r
534 * アクティブエディタ変更通知を行う。
\r
536 private void fireChangeActive(){
\r
537 ChangeEvent event = new ChangeEvent(this);
\r
539 ChangeListener[] listeners =
\r
540 this.listenerList.getListeners(ChangeListener.class);
\r
541 for(ChangeListener listener : listeners){
\r
542 listener.stateChanged(event);
\r
550 * エディタのフォーカス取得とともにアクティブ状態にする。
\r
551 * @param event {@inheritDoc}
\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
558 Document document = textComp.getDocument();
\r
559 TalkEditor editor = getEditorFromDocument(document);
\r
561 setActiveEditor(editor);
\r
568 * @param event {@inheritDoc}
\r
570 public void focusLost(FocusEvent event){
\r
577 * @return {@inheritDoc}
\r
579 public Dimension getPreferredScrollableViewportSize(){
\r
580 Dimension result = getPreferredSize();
\r
586 * 横スクロールバーを極力出さないようレイアウトでがんばる。
\r
587 * @return {@inheritDoc}
\r
589 public boolean getScrollableTracksViewportWidth(){
\r
595 * 縦スクロールバーを出しても良いのでレイアウトでがんばらない。
\r
596 * @return {@inheritDoc}
\r
598 public boolean getScrollableTracksViewportHeight(){
\r
604 * @param visibleRect {@inheritDoc}
\r
605 * @param orientation {@inheritDoc}
\r
606 * @param direction {@inheritDoc}
\r
607 * @return {@inheritDoc}
\r
609 public int getScrollableBlockIncrement(Rectangle visibleRect,
\r
612 if(orientation == SwingConstants.VERTICAL){
\r
613 return visibleRect.height;
\r
620 * @param visibleRect {@inheritDoc}
\r
621 * @param orientation {@inheritDoc}
\r
622 * @param direction {@inheritDoc}
\r
623 * @return {@inheritDoc}
\r
625 public int getScrollableUnitIncrement(Rectangle visibleRect,
\r
628 return 30; // TODO フォント高の1.5倍くらい?
\r
632 * エディタ内のカーソル移動を監視するための、
\r
633 * カスタム化したナビゲーションフィルター。
\r
634 * 必要に応じてエディタ間カーソル移動を行う。
\r
636 private class CustomNavigation extends NavigationFilter{
\r
641 public CustomNavigation(){
\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
659 public int getNextVisualPositionFrom(JTextComponent text,
\r
664 throws BadLocationException {
\r
665 int result = super.getNextVisualPositionFrom(text,
\r
670 if(result != pos) return result;
\r
673 case SwingConstants.WEST:
\r
674 case SwingConstants.NORTH:
\r
677 case SwingConstants.EAST:
\r
678 case SwingConstants.SOUTH:
\r
690 * エディタの内容変更を監視し、随時エディタ間調整を行う。
\r
692 private class DocWatcher implements DocumentListener{
\r
697 public DocWatcher(){
\r
704 * @param event {@inheritDoc}
\r
706 public void changedUpdate(DocumentEvent event){
\r
707 detachAdjustTask(event);
\r
713 * @param event {@inheritDoc}
\r
715 public void insertUpdate(DocumentEvent event){
\r
716 detachAdjustTask(event);
\r
722 * @param event {@inheritDoc}
\r
724 public void removeUpdate(DocumentEvent event){
\r
725 detachAdjustTask(event);
\r