4 * License : The MIT License
5 * Copyright(c) 2008 olyutorskii
8 package jp.sfjp.jindolf.editor;
10 import java.awt.Dimension;
11 import java.awt.EventQueue;
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;
36 * ※ このクラスはすべてシングルスレッドモデルで作られている。
38 @SuppressWarnings("serial")
39 public class EditArray extends JPanel
40 implements Scrollable,
43 private static final int MAX_EDITORS = 50;
45 private final List<TalkEditor> editorList = new ArrayList<TalkEditor>();
46 private boolean onAdjusting = false;
48 private final NavigationFilter keyNavigator = new CustomNavigation();
49 private final DocumentListener documentListener = new DocWatcher();
51 private TalkEditor activeEditor;
53 private Font textFont;
63 LayoutManager layout = new GridBagLayout();
66 TalkEditor firstEditor = incrementTalkEditor();
67 setActiveEditor(firstEditor);
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);
83 if(this.textFont == null){
84 this.textFont = editor.getTextFont();
86 editor.setTextFont(this.textFont);
96 private TalkEditor incrementTalkEditor(){
97 TalkEditor editor = createTalkEditor();
99 GridBagConstraints constraints = new GridBagConstraints();
101 constraints.gridx = 0;
102 constraints.gridy = GridBagConstraints.RELATIVE;
104 constraints.gridwidth = GridBagConstraints.REMAINDER;
105 constraints.gridheight = 1;
107 constraints.weightx = 1.0;
108 constraints.weighty = 0.0;
110 constraints.fill = GridBagConstraints.HORIZONTAL;
111 constraints.anchor = GridBagConstraints.NORTHEAST;
113 add(editor, constraints);
115 this.editorList.add(editor);
117 int sequenceNumber = this.editorList.size();
118 editor.setSequenceNumber(sequenceNumber);
124 * 1から始まる通し番号指定でエディタを取得する。
125 * 存在しない通し番号が指定された場合は新たにエディタが追加される。
126 * @param sequenceNumber 通し番号
129 private TalkEditor getTalkEditor(int sequenceNumber){
130 while(this.editorList.size() < sequenceNumber){
131 incrementTalkEditor();
134 TalkEditor result = this.editorList.get(sequenceNumber - 1);
140 * 指定したエディタの次の通し番号を持つエディタを返す。
145 private TalkEditor nextEditor(TalkEditor editor){
146 int sequenceNumber = editor.getSequenceNumber();
147 TalkEditor nextEditor = getTalkEditor(sequenceNumber + 1);
152 * 指定したエディタの前の通し番号を持つエディタを返す。
155 * 最初のエディタ(通し番号1)が指定されればnullを返す。
157 private TalkEditor prevEditor(TalkEditor editor){
158 int sequenceNumber = editor.getSequenceNumber();
159 if(sequenceNumber <= 1) return null;
160 TalkEditor prevEditor = getTalkEditor(sequenceNumber - 1);
165 * 指定したエディタがエディタ集合の最後のエディタか判定する。
167 * @return 最後のエディタならtrue
169 private boolean isLastEditor(TalkEditor editor){
170 int seqNo = editor.getSequenceNumber();
171 int size = this.editorList.size();
172 if(seqNo >= size) return true;
177 * Documentからその持ち主であるエディタを取得する。
178 * @param document Documentインスタンス
179 * @return 持ち主のエディタ。見つからなければnull。
181 private TalkEditor getEditorFromDocument(Document document){
182 for(TalkEditor editor : this.editorList){
183 if(editor.getDocument() == document) return editor;
189 * エディタ集合から任意のエディタを除く。
193 private void removeEditor(TalkEditor editor){
194 if(editor.getParent() != this) return;
196 int seqNo = editor.getSequenceNumber();
197 if(seqNo <= 1) return;
198 TalkEditor prevEditor = prevEditor(editor);
199 if(editor.isActive()){
200 setActiveEditor(prevEditor);
202 if(editor.hasEditorFocus()){
203 prevEditor.requestEditorFocus();
206 this.editorList.remove(seqNo - 1);
208 editor.setNavigationFilter(null);
209 editor.removeTextFocusListener(this);
210 Document document = editor.getDocument();
211 document.removeDocumentListener(this.documentListener);
218 for(TalkEditor newEditor : this.editorList){
219 newEditor.setSequenceNumber(renumber++);
226 * エディタ間文字調整タスクをディスパッチスレッドとして事後投入する。
227 * エディタ間文字調整タスクが実行中であれば何もしない。
228 * きっかけとなったエディタ上でIME操作が確定していなければ何もしない。
229 * @param triggerEvent ドキュメント変更イベント
231 private void detachAdjustTask(DocumentEvent triggerEvent){
232 if(this.onAdjusting) return;
234 Document document = triggerEvent.getDocument();
235 final TalkEditor triggerEditor = getEditorFromDocument(document);
236 if(triggerEditor.onIMEoperation()) return;
238 this.onAdjusting = true;
240 EventQueue.invokeLater(new Runnable(){
243 adjustTask(triggerEditor);
245 EditArray.this.onAdjusting = false;
256 * @param triggerEditor タスク実行のきっかけとなったエディタ
258 private void adjustTask(TalkEditor triggerEditor){
259 int initCaretPos = triggerEditor.getCaretPosition();
261 TalkEditor newFocus = null;
262 int newCaretPos = -1;
264 TalkEditor current = triggerEditor;
268 if( ! isLastEditor(current) ){
269 next = nextEditor(current);
270 String nextContents = next.getText();
271 int nextLength = nextContents.length();
273 current.appendTail(nextContents);
274 String rest = current.chopRest();
276 if(rest == null) restLength = 0;
277 else restLength = rest.length();
279 int chopLength = nextLength - restLength;
281 next.chopHead(chopLength);
282 }else if(chopLength < 0){
283 rest = rest.substring(0, -chopLength);
284 next.appendHead(rest);
286 if(newFocus == null){
288 newCaretPos = initCaretPos;
293 String rest = current.chopRest();
294 if(rest == null || this.editorList.size() >= MAX_EDITORS){
295 if(newFocus == null){
297 if(current.getTextLength() >= initCaretPos){
298 newCaretPos = initCaretPos;
300 newCaretPos = current.getTextLength();
305 next = nextEditor(current);
306 next.appendHead(rest);
309 if(newFocus == null){
310 int currentLength = current.getTextLength();
311 if(initCaretPos >= currentLength){
312 initCaretPos -= currentLength;
315 newCaretPos = initCaretPos;
322 if(newFocus != null){
323 newFocus.requestEditorFocus();
324 newFocus.setCaretPosition(newCaretPos);
333 * エディタ集合末尾の空エディタを切り詰める。
334 * ただし最初のエディタ(通し番号1)は削除されない。
335 * フォーカスを持つエディタが削除された場合は、
336 * 削除されなかった最後のエディタにフォーカスが移る。
338 private void adjustEditorsTail(){
339 int editorNum = this.editorList.size();
340 if(editorNum <= 0) return;
341 TalkEditor lastEditor = this.editorList.get(editorNum - 1);
343 TalkEditor prevlostEditor = null;
345 boolean lostFocusedEditor = false;
348 int textLength = lastEditor.getTextLength();
349 int seqNo = lastEditor.getSequenceNumber();
351 if(lostFocusedEditor){
352 prevlostEditor = lastEditor;
355 if(textLength > 0) break;
356 if(seqNo <= 1) break;
358 if(lastEditor.hasEditorFocus()) lostFocusedEditor = true;
359 removeEditor(lastEditor);
361 lastEditor = prevEditor(lastEditor); // TODO ちょっと変
364 if(prevlostEditor != null){
365 int textLength = prevlostEditor.getTextLength();
366 prevlostEditor.requestEditorFocus();
367 prevlostEditor.setCaretPosition(textLength);
377 public TalkEditor getFocusedTalkEditor(){
378 for(TalkEditor editor : this.editorList){
379 if(editor.hasEditorFocus()) return editor;
385 * フォーカスを持つエディタの次エディタがあればフォーカスを移し、
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();
399 * フォーカスを持つエディタの前エディタがあればフォーカスを移し、
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();
414 * 同時にアクティブなエディタは一つのみ。
415 * @param editor アクティブにするエディタ
417 private void setActiveEditor(TalkEditor editor){
418 if(this.activeEditor != null){
419 this.activeEditor.setActive(false);
422 this.activeEditor = editor;
424 if(this.activeEditor != null){
425 this.activeEditor.setActive(true);
435 * @return アクティブなエディタ。
437 public TalkEditor getActiveEditor(){
438 return this.activeEditor;
445 public CharSequence getAllText(){
446 StringBuilder result = new StringBuilder();
448 for(TalkEditor editor : this.editorList){
449 String text = editor.getText();
457 * 先頭エディタの0文字目から字を詰め込む。
458 * 2番目移行のエディタへはみ出すかもしれない。
461 public void setAllText(CharSequence seq){
462 TalkEditor firstEditor = getTalkEditor(1);
463 Document doc = firstEditor.getDocument();
465 doc.insertString(0, seq.toString(), null);
466 }catch(BadLocationException e){
475 public void clearAllEditor(){
476 int editorNum = this.editorList.size();
477 if(editorNum <= 0) return;
479 TalkEditor lastEditor = this.editorList.get(editorNum - 1);
481 removeEditor(lastEditor);
482 lastEditor = prevEditor(lastEditor);
483 if(lastEditor == null) break;
486 TalkEditor firstEditor = getTalkEditor(1);
487 firstEditor.clearText();
488 setActiveEditor(firstEditor);
495 * @param textFont フォント
497 public void setTextFont(Font textFont){
498 this.textFont = textFont;
499 for(TalkEditor editor : this.editorList){
500 editor.setTextFont(this.textFont);
511 public Font getTextFont(){
512 return this.textFont;
516 * アクティブエディタ変更通知用リスナの登録。
517 * @param listener リスナ
519 public void addChangeListener(ChangeListener listener){
520 this.listenerList.add(ChangeListener.class, listener);
525 * アクティブエディタ変更通知用リスナの削除。
526 * @param listener リスナ
528 public void removeChangeListener(ChangeListener listener){
529 this.listenerList.remove(ChangeListener.class, listener);
536 private void fireChangeActive(){
537 ChangeEvent event = new ChangeEvent(this);
539 ChangeListener[] listeners =
540 this.listenerList.getListeners(ChangeListener.class);
541 for(ChangeListener listener : listeners){
542 listener.stateChanged(event);
550 * エディタのフォーカス取得とともにアクティブ状態にする。
551 * @param event {@inheritDoc}
554 public void focusGained(FocusEvent event){
555 Object source = event.getSource();
556 if( ! (source instanceof JTextComponent) ) return;
557 JTextComponent textComp = (JTextComponent) source;
559 Document document = textComp.getDocument();
560 TalkEditor editor = getEditorFromDocument(document);
562 setActiveEditor(editor);
569 * @param event {@inheritDoc}
572 public void focusLost(FocusEvent event){
579 * @return {@inheritDoc}
582 public Dimension getPreferredScrollableViewportSize(){
583 Dimension result = getPreferredSize();
589 * 横スクロールバーを極力出さないようレイアウトでがんばる。
590 * @return {@inheritDoc}
593 public boolean getScrollableTracksViewportWidth(){
599 * 縦スクロールバーを出しても良いのでレイアウトでがんばらない。
600 * @return {@inheritDoc}
603 public boolean getScrollableTracksViewportHeight(){
609 * @param visibleRect {@inheritDoc}
610 * @param orientation {@inheritDoc}
611 * @param direction {@inheritDoc}
612 * @return {@inheritDoc}
615 public int getScrollableBlockIncrement(Rectangle visibleRect,
618 if(orientation == SwingConstants.VERTICAL){
619 return visibleRect.height;
626 * @param visibleRect {@inheritDoc}
627 * @param orientation {@inheritDoc}
628 * @param direction {@inheritDoc}
629 * @return {@inheritDoc}
632 public int getScrollableUnitIncrement(Rectangle visibleRect,
635 return 30; // TODO フォント高の1.5倍くらい?
639 * エディタ内のカーソル移動を監視するための、
640 * カスタム化したナビゲーションフィルター。
641 * 必要に応じてエディタ間カーソル移動を行う。
643 private class CustomNavigation extends NavigationFilter{
648 public CustomNavigation(){
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}
666 public int getNextVisualPositionFrom(JTextComponent text,
671 throws BadLocationException {
672 int result = super.getNextVisualPositionFrom(text,
677 if(result != pos) return result;
680 case SwingConstants.WEST:
681 case SwingConstants.NORTH:
684 case SwingConstants.EAST:
685 case SwingConstants.SOUTH:
697 * エディタの内容変更を監視し、随時エディタ間調整を行う。
699 private class DocWatcher implements DocumentListener{
711 * @param event {@inheritDoc}
714 public void changedUpdate(DocumentEvent event){
715 detachAdjustTask(event);
721 * @param event {@inheritDoc}
724 public void insertUpdate(DocumentEvent event){
725 detachAdjustTask(event);
731 * @param event {@inheritDoc}
734 public void removeUpdate(DocumentEvent event){
735 detachAdjustTask(event);