OSDN Git Service

Merge branch 'Branch_release-'
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / editor / EditArray.java
-/*\r
- * エディタ集合の操作\r
- *\r
- * Copyright(c) 2008 olyutorskii\r
- * $Id: EditArray.java 953 2009-12-06 16:42:14Z olyutorskii $\r
- */\r
-\r
-package jp.sourceforge.jindolf;\r
-\r
-import java.awt.Dimension;\r
-import java.awt.EventQueue;\r
-import java.awt.Font;\r
-import java.awt.GridBagConstraints;\r
-import java.awt.GridBagLayout;\r
-import java.awt.LayoutManager;\r
-import java.awt.Rectangle;\r
-import java.awt.event.FocusEvent;\r
-import java.awt.event.FocusListener;\r
-import java.util.ArrayList;\r
-import java.util.List;\r
-import javax.swing.JPanel;\r
-import javax.swing.Scrollable;\r
-import javax.swing.SwingConstants;\r
-import javax.swing.event.ChangeEvent;\r
-import javax.swing.event.ChangeListener;\r
-import javax.swing.event.DocumentEvent;\r
-import javax.swing.event.DocumentListener;\r
-import javax.swing.text.BadLocationException;\r
-import javax.swing.text.Document;\r
-import javax.swing.text.JTextComponent;\r
-import javax.swing.text.NavigationFilter;\r
-import javax.swing.text.Position.Bias;\r
-\r
-/**\r
- * エディタ集合の操作。\r
- * ※ このクラスはすべてシングルスレッドモデルで作られている。\r
- */\r
-@SuppressWarnings("serial")\r
-public class EditArray extends JPanel\r
-                       implements Scrollable,\r
-                                  FocusListener {\r
-\r
-    private static final int MAX_EDITORS = 50;\r
-\r
-    private final List<TalkEditor> editorList = new ArrayList<TalkEditor>();\r
-    private boolean onAdjusting = false;\r
-\r
-    private final NavigationFilter keyNavigator = new CustomNavigation();\r
-    private final DocumentListener documentListener = new DocWatcher();\r
-\r
-    private TalkEditor activeEditor;\r
-\r
-    private Font textFont;\r
-\r
-    /**\r
-     * コンストラクタ。\r
-     */\r
-    public EditArray(){\r
-        super();\r
-\r
-        setOpaque(false);\r
-\r
-        LayoutManager layout = new GridBagLayout();\r
-        setLayout(layout);\r
-\r
-        TalkEditor firstEditor = incrementTalkEditor();\r
-        setActiveEditor(firstEditor);\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 個別エディタの生成を行う。\r
-     * @return エディタ\r
-     */\r
-    private TalkEditor createTalkEditor(){\r
-        TalkEditor editor = new TalkEditor();\r
-        editor.setNavigationFilter(this.keyNavigator);\r
-        editor.addTextFocusListener(this);\r
-        Document document = editor.getDocument();\r
-        document.addDocumentListener(this.documentListener);\r
-\r
-        if(this.textFont == null){\r
-            this.textFont = editor.getTextFont();\r
-        }else{\r
-            editor.setTextFont(this.textFont);\r
-        }\r
-\r
-        return editor;\r
-    }\r
-\r
-    /**\r
-     * エディタ集合を一つ増やす。\r
-     * @return 増えたエディタ\r
-     */\r
-    private TalkEditor incrementTalkEditor(){\r
-        TalkEditor editor = createTalkEditor();\r
-\r
-        GridBagConstraints constraints = new GridBagConstraints();\r
-\r
-        constraints.gridx = 0;\r
-        constraints.gridy = GridBagConstraints.RELATIVE;\r
-\r
-        constraints.gridwidth = GridBagConstraints.REMAINDER;\r
-        constraints.gridheight = 1;\r
-\r
-        constraints.weightx = 1.0;\r
-        constraints.weighty = 0.0;\r
-\r
-        constraints.fill = GridBagConstraints.HORIZONTAL;\r
-        constraints.anchor = GridBagConstraints.NORTHEAST;\r
-\r
-        add(editor, constraints);\r
-\r
-        this.editorList.add(editor);\r
-\r
-        int sequenceNumber = this.editorList.size();\r
-        editor.setSequenceNumber(sequenceNumber);\r
-\r
-        return editor;\r
-    }\r
-\r
-    /**\r
-     * 1から始まる通し番号指定でエディタを取得する。\r
-     * 存在しない通し番号が指定された場合は新たにエディタが追加される。\r
-     * @param sequenceNumber 通し番号\r
-     * @return エディタ\r
-     */\r
-    private TalkEditor getTalkEditor(int sequenceNumber){\r
-        while(this.editorList.size() < sequenceNumber){\r
-            incrementTalkEditor();\r
-        }\r
-\r
-        TalkEditor result = this.editorList.get(sequenceNumber - 1);\r
-\r
-        return result;\r
-    }\r
-\r
-    /**\r
-     * 指定したエディタの次の通し番号を持つエディタを返す。\r
-     * エディタがなければ追加される。\r
-     * @param editor エディタ\r
-     * @return 次のエディタ\r
-     */\r
-    private TalkEditor nextEditor(TalkEditor editor){\r
-        int sequenceNumber = editor.getSequenceNumber();\r
-        TalkEditor nextEditor = getTalkEditor(sequenceNumber + 1);\r
-        return nextEditor;\r
-    }\r
-\r
-    /**\r
-     * 指定したエディタの前の通し番号を持つエディタを返す。\r
-     * @param editor エディタ\r
-     * @return 前のエディタ。\r
-     * 最初のエディタ(通し番号1)が指定されればnullを返す。\r
-     */\r
-    private TalkEditor prevEditor(TalkEditor editor){\r
-        int sequenceNumber = editor.getSequenceNumber();\r
-        if(sequenceNumber <= 1) return null;\r
-        TalkEditor prevEditor = getTalkEditor(sequenceNumber - 1);\r
-        return prevEditor;\r
-    }\r
-\r
-    /**\r
-     * 指定したエディタがエディタ集合の最後のエディタか判定する。\r
-     * @param editor エディタ\r
-     * @return 最後のエディタならtrue\r
-     */\r
-    private boolean isLastEditor(TalkEditor editor){\r
-        int seqNo = editor.getSequenceNumber();\r
-        int size = this.editorList.size();\r
-        if(seqNo >= size) return true;\r
-        return false;\r
-    }\r
-\r
-    /**\r
-     * Documentからその持ち主であるエディタを取得する。\r
-     * @param document Documentインスタンス\r
-     * @return 持ち主のエディタ。見つからなければnull。\r
-     */\r
-    private TalkEditor getEditorFromDocument(Document document){\r
-        for(TalkEditor editor : this.editorList){\r
-            if(editor.getDocument() == document) return editor;\r
-        }\r
-        return null;\r
-    }\r
-\r
-    /**\r
-     * エディタ集合から任意のエディタを除く。\r
-     * ただし最初のエディタは消去不可。\r
-     * @param editor エディタ\r
-     */\r
-    private void removeEditor(TalkEditor editor){\r
-        if(editor.getParent() != this) return;\r
-\r
-        int seqNo = editor.getSequenceNumber();\r
-        if(seqNo <= 1) return;\r
-        TalkEditor prevEditor = prevEditor(editor);\r
-        if(editor.isActive()){\r
-            setActiveEditor(prevEditor);\r
-        }\r
-        if(editor.hasEditorFocus()){\r
-            prevEditor.requestEditorFocus();\r
-        }\r
-\r
-        this.editorList.remove(seqNo - 1);\r
-\r
-        editor.setNavigationFilter(null);\r
-        editor.removeTextFocusListener(this);\r
-        Document document = editor.getDocument();\r
-        document.removeDocumentListener(this.documentListener);\r
-        editor.clearText();\r
-\r
-        remove(editor);\r
-        revalidate();\r
-\r
-        int renumber = 1;\r
-        for(TalkEditor newEditor : this.editorList){\r
-            newEditor.setSequenceNumber(renumber++);\r
-        }\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * エディタ間文字調整タスクをディスパッチスレッドとして事後投入する。\r
-     * エディタ間文字調整タスクが実行中であれば何もしない。\r
-     * きっかけとなったエディタ上でIME操作が確定していなければ何もしない。\r
-     * @param triggerEvent ドキュメント変更イベント\r
-     */\r
-    private void detachAdjustTask(DocumentEvent triggerEvent){\r
-        if(this.onAdjusting) return;\r
-\r
-        Document document = triggerEvent.getDocument();\r
-        final TalkEditor triggerEditor = getEditorFromDocument(document);\r
-        if(triggerEditor.onIMEoperation()) return;\r
-\r
-        this.onAdjusting = true;\r
-\r
-        EventQueue.invokeLater(new Runnable(){\r
-            public void run(){\r
-                try{\r
-                    adjustTask(triggerEditor);\r
-                }finally{\r
-                    EditArray.this.onAdjusting = false;\r
-                }\r
-                return;\r
-            }\r
-        });\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * エディタ間文字調整タスク本体。\r
-     * @param triggerEditor タスク実行のきっかけとなったエディタ\r
-     */\r
-    private void adjustTask(TalkEditor triggerEditor){\r
-        int initCaretPos = triggerEditor.getCaretPosition();\r
-\r
-        TalkEditor newFocus = null;\r
-        int newCaretPos = -1;\r
-\r
-        TalkEditor current = triggerEditor;\r
-        for(;;){\r
-            TalkEditor next;\r
-\r
-            if( ! isLastEditor(current) ){\r
-                next = nextEditor(current);\r
-                String nextContents = next.getText();\r
-                int nextLength = nextContents.length();\r
-\r
-                current.appendTail(nextContents);\r
-                String rest = current.chopRest();\r
-                int restLength;\r
-                if(rest == null) restLength = 0;\r
-                else             restLength = rest.length();\r
-\r
-                int chopLength = nextLength - restLength;\r
-                if(chopLength > 0){\r
-                    next.chopHead(chopLength);\r
-                }else if(chopLength < 0){\r
-                    rest = rest.substring(0, -chopLength);\r
-                    next.appendHead(rest);\r
-                }else{\r
-                    if(newFocus == null){\r
-                        newFocus = current;\r
-                        newCaretPos = initCaretPos;\r
-                    }\r
-                    break;\r
-                }\r
-            }else{\r
-                String rest = current.chopRest();\r
-                if(rest == null || this.editorList.size() >= MAX_EDITORS){\r
-                    if(newFocus == null){\r
-                        newFocus = current;\r
-                        if(current.getTextLength() >= initCaretPos){\r
-                            newCaretPos = initCaretPos;\r
-                        }else{\r
-                            newCaretPos = current.getTextLength();\r
-                        }\r
-                    }\r
-                    break;\r
-                }\r
-                next = nextEditor(current);\r
-                next.appendHead(rest);\r
-            }\r
-\r
-            if(newFocus == null){\r
-                int currentLength = current.getTextLength();\r
-                if(initCaretPos >= currentLength){\r
-                    initCaretPos -= currentLength;\r
-                }else{\r
-                    newFocus = current;\r
-                    newCaretPos = initCaretPos;\r
-                }\r
-            }\r
-\r
-            current = next;\r
-        }\r
-\r
-        if(newFocus != null){\r
-            newFocus.requestEditorFocus();\r
-            newFocus.setCaretPosition(newCaretPos);\r
-        }\r
-\r
-        adjustEditorsTail();\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * エディタ集合末尾の空エディタを切り詰める。\r
-     * ただし最初のエディタ(通し番号1)は削除されない。\r
-     * フォーカスを持つエディタが削除された場合は、\r
-     * 削除されなかった最後のエディタにフォーカスが移る。\r
-     */\r
-    private void adjustEditorsTail(){\r
-        int editorNum = this.editorList.size();\r
-        if(editorNum <= 0) return;\r
-        TalkEditor lastEditor = this.editorList.get(editorNum - 1);\r
-\r
-        TalkEditor prevlostEditor = null;\r
-\r
-        boolean lostFocusedEditor = false;\r
-\r
-        for(;;){\r
-            int textLength = lastEditor.getTextLength();\r
-            int seqNo = lastEditor.getSequenceNumber();\r
-\r
-            if(lostFocusedEditor){\r
-                prevlostEditor = lastEditor;\r
-            }\r
-\r
-            if(textLength > 0) break;\r
-            if(seqNo <= 1) break;\r
-\r
-            if(lastEditor.hasEditorFocus()) lostFocusedEditor = true;\r
-            removeEditor(lastEditor);\r
-\r
-            lastEditor = prevEditor(lastEditor); // TODO ちょっと変\r
-        }\r
-\r
-        if(prevlostEditor != null){\r
-            int textLength = prevlostEditor.getTextLength();\r
-            prevlostEditor.requestEditorFocus();\r
-            prevlostEditor.setCaretPosition(textLength);\r
-        }\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * フォーカスを持つエディタを取得する。\r
-     * @return エディタ\r
-     */\r
-    public TalkEditor getFocusedTalkEditor(){\r
-        for(TalkEditor editor : this.editorList){\r
-            if(editor.hasEditorFocus()) return editor;\r
-        }\r
-        return null;\r
-    }\r
-\r
-    /**\r
-     * フォーカスを持つエディタの次エディタがあればフォーカスを移し、\r
-     * カレット位置を0にする。\r
-     */\r
-    // TODO エディタのスクロール位置調整が必要。\r
-    public void forwardEditor(){\r
-        TalkEditor editor = getFocusedTalkEditor();\r
-        if(isLastEditor(editor)) return;\r
-        TalkEditor next = nextEditor(editor);\r
-        next.setCaretPosition(0);\r
-        next.requestEditorFocus();\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * フォーカスを持つエディタの前エディタがあればフォーカスを移し、\r
-     * カレット位置を末尾に置く。\r
-     */\r
-    public void backwardEditor(){\r
-        TalkEditor editor = getFocusedTalkEditor();\r
-        TalkEditor prev = prevEditor(editor);\r
-        if(prev == null) return;\r
-        int length = prev.getTextLength();\r
-        prev.setCaretPosition(length);\r
-        prev.requestEditorFocus();\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 任意のエディタをアクティブにする。\r
-     * 同時にアクティブなエディタは一つのみ。\r
-     * @param editor アクティブにするエディタ\r
-     */\r
-    private void setActiveEditor(TalkEditor editor){\r
-        if(this.activeEditor != null){\r
-            this.activeEditor.setActive(false);\r
-        }\r
-\r
-        this.activeEditor = editor;\r
-\r
-        if(this.activeEditor != null){\r
-            this.activeEditor.setActive(true);\r
-        }\r
-\r
-        fireChangeActive();\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * アクティブなエディタを返す。\r
-     * @return アクティブなエディタ。\r
-     */\r
-    public TalkEditor getActiveEditor(){\r
-        return this.activeEditor;\r
-    }\r
-\r
-    /**\r
-     * 全発言を連結した文字列を返す。\r
-     * @return 連結文字列\r
-     */\r
-    public CharSequence getAllText(){\r
-        StringBuilder result = new StringBuilder();\r
-\r
-        for(TalkEditor editor : this.editorList){\r
-            String text = editor.getText();\r
-            result.append(text);\r
-        }\r
-\r
-        return result;\r
-    }\r
-\r
-    /**\r
-     * 先頭エディタの0文字目から字を詰め込む。\r
-     * 2番目移行のエディタへはみ出すかもしれない。\r
-     * @param seq 詰め込む文字列\r
-     */\r
-    public void setAllText(CharSequence seq){\r
-        TalkEditor firstEditor = getTalkEditor(1);\r
-        Document doc = firstEditor.getDocument();\r
-        try{\r
-            doc.insertString(0, seq.toString(), null);\r
-        }catch(BadLocationException e){\r
-            assert false;\r
-        }\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * 全エディタをクリアする。\r
-     */\r
-    public void clearAllEditor(){\r
-        int editorNum = this.editorList.size();\r
-        if(editorNum <= 0) return;\r
-\r
-        TalkEditor lastEditor = this.editorList.get(editorNum - 1);\r
-        for(;;){\r
-            removeEditor(lastEditor);\r
-            lastEditor = prevEditor(lastEditor);\r
-            if(lastEditor == null) break;\r
-        }\r
-\r
-        TalkEditor firstEditor = getTalkEditor(1);\r
-        firstEditor.clearText();\r
-        setActiveEditor(firstEditor);\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * テキスト編集用フォントを指定する。\r
-     * @param textFont フォント\r
-     */\r
-    public void setTextFont(Font textFont){\r
-        this.textFont = textFont;\r
-        for(TalkEditor editor : this.editorList){\r
-            editor.setTextFont(this.textFont);\r
-            editor.repaint();\r
-        }\r
-        revalidate();\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * テキスト編集用フォントを取得する。\r
-     * @return フォント\r
-     */\r
-    public Font getTextFont(){\r
-        return this.textFont;\r
-    }\r
-\r
-    /**\r
-     * アクティブエディタ変更通知用リスナの登録。\r
-     * @param listener リスナ\r
-     */\r
-    public void addChangeListener(ChangeListener listener){\r
-        this.listenerList.add(ChangeListener.class, listener);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * アクティブエディタ変更通知用リスナの削除。\r
-     * @param listener リスナ\r
-     */\r
-    public void removeChangeListener(ChangeListener listener){\r
-        this.listenerList.remove(ChangeListener.class, listener);\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * アクティブエディタ変更通知を行う。\r
-     */\r
-    private void fireChangeActive(){\r
-        ChangeEvent event = new ChangeEvent(this);\r
-\r
-        ChangeListener[] listeners =\r
-                this.listenerList.getListeners(ChangeListener.class);\r
-        for(ChangeListener listener : listeners){\r
-            listener.stateChanged(event);\r
-        }\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * {@inheritDoc}\r
-     * エディタのフォーカス取得とともにアクティブ状態にする。\r
-     * @param event {@inheritDoc}\r
-     */\r
-    public void focusGained(FocusEvent event){\r
-        Object source = event.getSource();\r
-        if( ! (source instanceof JTextComponent) ) return;\r
-        JTextComponent textComp = (JTextComponent) source;\r
-\r
-        Document document = textComp.getDocument();\r
-        TalkEditor editor = getEditorFromDocument(document);\r
-\r
-        setActiveEditor(editor);\r
-\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * {@inheritDoc}\r
-     * @param event {@inheritDoc}\r
-     */\r
-    public void focusLost(FocusEvent event){\r
-        // NOTHING\r
-        return;\r
-    }\r
-\r
-    /**\r
-     * {@inheritDoc}\r
-     * @return {@inheritDoc}\r
-     */\r
-    public Dimension getPreferredScrollableViewportSize(){\r
-        Dimension result = getPreferredSize();\r
-        return result;\r
-    }\r
-\r
-    /**\r
-     * {@inheritDoc}\r
-     * 横スクロールバーを極力出さないようレイアウトでがんばる。\r
-     * @return {@inheritDoc}\r
-     */\r
-    public boolean getScrollableTracksViewportWidth(){\r
-        return true;\r
-    }\r
-\r
-    /**\r
-     * {@inheritDoc}\r
-     * 縦スクロールバーを出しても良いのでレイアウトでがんばらない。\r
-     * @return {@inheritDoc}\r
-     */\r
-    public boolean getScrollableTracksViewportHeight(){\r
-        return false;\r
-    }\r
-\r
-    /**\r
-     *  {@inheritDoc}\r
-     * @param visibleRect {@inheritDoc}\r
-     * @param orientation {@inheritDoc}\r
-     * @param direction {@inheritDoc}\r
-     * @return {@inheritDoc}\r
-     */\r
-    public int getScrollableBlockIncrement(Rectangle visibleRect,\r
-                                           int orientation,\r
-                                           int direction ){\r
-        if(orientation == SwingConstants.VERTICAL){\r
-            return visibleRect.height;\r
-        }\r
-        return 10;\r
-    }\r
-\r
-    /**\r
-     * {@inheritDoc}\r
-     * @param visibleRect {@inheritDoc}\r
-     * @param orientation {@inheritDoc}\r
-     * @param direction {@inheritDoc}\r
-     * @return {@inheritDoc}\r
-     */\r
-    public int getScrollableUnitIncrement(Rectangle visibleRect,\r
-                                          int orientation,\r
-                                          int direction ){\r
-        return 30; // TODO フォント高の1.5倍くらい?\r
-    }\r
-\r
-    /**\r
-     * エディタ内のカーソル移動を監視するための、\r
-     * カスタム化したナビゲーションフィルター。\r
-     * 必要に応じてエディタ間カーソル移動を行う。\r
-     */\r
-    private class CustomNavigation extends NavigationFilter{\r
-\r
-        /**\r
-         * コンストラクタ。\r
-         */\r
-        public CustomNavigation(){\r
-            super();\r
-            return;\r
-        }\r
-\r
-        /**\r
-         * {@inheritDoc}\r
-         * カーソル移動が行き詰まった場合、\r
-         * 隣接するエディタ間でカーソル移動を行う。\r
-         * @param text {@inheritDoc}\r
-         * @param pos {@inheritDoc}\r
-         * @param bias {@inheritDoc}\r
-         * @param direction {@inheritDoc}\r
-         * @param biasRet {@inheritDoc}\r
-         * @return {@inheritDoc}\r
-         * @throws javax.swing.text.BadLocationException {@inheritDoc}\r
-         */\r
-        @Override\r
-        public int getNextVisualPositionFrom(JTextComponent text,\r
-                                                 int pos,\r
-                                                 Bias bias,\r
-                                                 int direction,\r
-                                                 Bias[] biasRet )\r
-                                                 throws BadLocationException {\r
-            int result = super.getNextVisualPositionFrom(text,\r
-                                                         pos,\r
-                                                         bias,\r
-                                                         direction,\r
-                                                         biasRet );\r
-            if(result != pos) return result;\r
-\r
-            switch(direction){\r
-            case SwingConstants.WEST:\r
-            case SwingConstants.NORTH:\r
-                backwardEditor();\r
-                break;\r
-            case SwingConstants.EAST:\r
-            case SwingConstants.SOUTH:\r
-                forwardEditor();\r
-                break;\r
-            default:\r
-                assert false;\r
-            }\r
-\r
-            return result;\r
-        }\r
-    }\r
-\r
-    /**\r
-     * エディタの内容変更を監視し、随時エディタ間調整を行う。\r
-     */\r
-    private class DocWatcher implements DocumentListener{\r
-\r
-        /**\r
-         * コンストラクタ。\r
-         */\r
-        public DocWatcher(){\r
-            super();\r
-            return;\r
-        }\r
-\r
-        /**\r
-         * {@inheritDoc}\r
-         * @param event {@inheritDoc}\r
-         */\r
-        public void changedUpdate(DocumentEvent event){\r
-            detachAdjustTask(event);\r
-            return;\r
-        }\r
-\r
-        /**\r
-         * {@inheritDoc}\r
-         * @param event {@inheritDoc}\r
-         */\r
-        public void insertUpdate(DocumentEvent event){\r
-            detachAdjustTask(event);\r
-            return;\r
-        }\r
-\r
-        /**\r
-         * {@inheritDoc}\r
-         * @param event {@inheritDoc}\r
-         */\r
-        public void removeUpdate(DocumentEvent event){\r
-            detachAdjustTask(event);\r
-            return;\r
-        }\r
-    }\r
-\r
-}\r
+/*
+ * エディタ集合の操作
+ *
+ * License : The MIT License
+ * Copyright(c) 2008 olyutorskii
+ */
+
+package jp.sfjp.jindolf.editor;
+
+import java.awt.Dimension;
+import java.awt.EventQueue;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.LayoutManager;
+import java.awt.Rectangle;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.util.ArrayList;
+import java.util.List;
+import javax.swing.JPanel;
+import javax.swing.Scrollable;
+import javax.swing.SwingConstants;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.JTextComponent;
+import javax.swing.text.NavigationFilter;
+import javax.swing.text.Position.Bias;
+
+/**
+ * エディタ集合の操作。
+ * ※ このクラスはすべてシングルスレッドモデルで作られている。
+ */
+@SuppressWarnings("serial")
+public class EditArray extends JPanel
+                       implements Scrollable,
+                                  FocusListener {
+
+    private static final int MAX_EDITORS = 50;
+
+    private final List<TalkEditor> editorList = new ArrayList<>();
+    private boolean onAdjusting = false;
+
+    private final NavigationFilter keyNavigator = new CustomNavigation();
+    private final DocumentListener documentListener = new DocWatcher();
+
+    private TalkEditor activeEditor;
+
+    private Font textFont;
+
+    /**
+     * コンストラクタ。
+     */
+    public EditArray(){
+        super();
+
+        setOpaque(false);
+
+        LayoutManager layout = new GridBagLayout();
+        setLayout(layout);
+
+        TalkEditor firstEditor = incrementTalkEditor();
+        setActiveEditor(firstEditor);
+
+        return;
+    }
+
+    /**
+     * 個別エディタの生成を行う。
+     * @return エディタ
+     */
+    private TalkEditor createTalkEditor(){
+        TalkEditor editor = new TalkEditor();
+        editor.setNavigationFilter(this.keyNavigator);
+        editor.addTextFocusListener(this);
+        Document document = editor.getDocument();
+        document.addDocumentListener(this.documentListener);
+
+        if(this.textFont == null){
+            this.textFont = editor.getTextFont();
+        }else{
+            editor.setTextFont(this.textFont);
+        }
+
+        return editor;
+    }
+
+    /**
+     * エディタ集合を一つ増やす。
+     * @return 増えたエディタ
+     */
+    private TalkEditor incrementTalkEditor(){
+        TalkEditor editor = createTalkEditor();
+
+        GridBagConstraints constraints = new GridBagConstraints();
+
+        constraints.gridx = 0;
+        constraints.gridy = GridBagConstraints.RELATIVE;
+
+        constraints.gridwidth = GridBagConstraints.REMAINDER;
+        constraints.gridheight = 1;
+
+        constraints.weightx = 1.0;
+        constraints.weighty = 0.0;
+
+        constraints.fill = GridBagConstraints.HORIZONTAL;
+        constraints.anchor = GridBagConstraints.NORTHEAST;
+
+        add(editor, constraints);
+
+        this.editorList.add(editor);
+
+        int sequenceNumber = this.editorList.size();
+        editor.setSequenceNumber(sequenceNumber);
+
+        return editor;
+    }
+
+    /**
+     * 1から始まる通し番号指定でエディタを取得する。
+     * 存在しない通し番号が指定された場合は新たにエディタが追加される。
+     * @param sequenceNumber 通し番号
+     * @return エディタ
+     */
+    private TalkEditor getTalkEditor(int sequenceNumber){
+        while(this.editorList.size() < sequenceNumber){
+            incrementTalkEditor();
+        }
+
+        TalkEditor result = this.editorList.get(sequenceNumber - 1);
+
+        return result;
+    }
+
+    /**
+     * 指定したエディタの次の通し番号を持つエディタを返す。
+     * エディタがなければ追加される。
+     * @param editor エディタ
+     * @return 次のエディタ
+     */
+    private TalkEditor nextEditor(TalkEditor editor){
+        int sequenceNumber = editor.getSequenceNumber();
+        TalkEditor nextEditor = getTalkEditor(sequenceNumber + 1);
+        return nextEditor;
+    }
+
+    /**
+     * 指定したエディタの前の通し番号を持つエディタを返す。
+     * @param editor エディタ
+     * @return 前のエディタ。
+     *     最初のエディタ(通し番号1)が指定されればnullを返す。
+     */
+    private TalkEditor prevEditor(TalkEditor editor){
+        int sequenceNumber = editor.getSequenceNumber();
+        if(sequenceNumber <= 1) return null;
+        TalkEditor prevEditor = getTalkEditor(sequenceNumber - 1);
+        return prevEditor;
+    }
+
+    /**
+     * 指定したエディタがエディタ集合の最後のエディタか判定する。
+     * @param editor エディタ
+     * @return 最後のエディタならtrue
+     */
+    private boolean isLastEditor(TalkEditor editor){
+        int seqNo = editor.getSequenceNumber();
+        int size = this.editorList.size();
+        if(seqNo >= size) return true;
+        return false;
+    }
+
+    /**
+     * Documentからその持ち主であるエディタを取得する。
+     * @param document Documentインスタンス
+     * @return 持ち主のエディタ。見つからなければnull。
+     */
+    private TalkEditor getEditorFromDocument(Document document){
+        for(TalkEditor editor : this.editorList){
+            if(editor.getDocument() == document) return editor;
+        }
+        return null;
+    }
+
+    /**
+     * エディタ集合から任意のエディタを除く。
+     * ただし最初のエディタは消去不可。
+     * @param editor エディタ
+     */
+    private void removeEditor(TalkEditor editor){
+        if(editor.getParent() != this) return;
+
+        int seqNo = editor.getSequenceNumber();
+        if(seqNo <= 1) return;
+        TalkEditor prevEditor = prevEditor(editor);
+        if(editor.isActive()){
+            setActiveEditor(prevEditor);
+        }
+        if(editor.hasEditorFocus()){
+            prevEditor.requestEditorFocus();
+        }
+
+        this.editorList.remove(seqNo - 1);
+
+        editor.setNavigationFilter(null);
+        editor.removeTextFocusListener(this);
+        Document document = editor.getDocument();
+        document.removeDocumentListener(this.documentListener);
+        editor.clearText();
+
+        remove(editor);
+        revalidate();
+
+        int renumber = 1;
+        for(TalkEditor newEditor : this.editorList){
+            newEditor.setSequenceNumber(renumber++);
+        }
+
+        return;
+    }
+
+    /**
+     * エディタ間文字調整タスクをディスパッチスレッドとして事後投入する。
+     * エディタ間文字調整タスクが実行中であれば何もしない。
+     * きっかけとなったエディタ上でIME操作が確定していなければ何もしない。
+     * @param triggerEvent ドキュメント変更イベント
+     */
+    private void detachAdjustTask(DocumentEvent triggerEvent){
+        if(this.onAdjusting) return;
+
+        Document document = triggerEvent.getDocument();
+        final TalkEditor triggerEditor = getEditorFromDocument(document);
+        if(triggerEditor.onIMEoperation()) return;
+
+        this.onAdjusting = true;
+
+        EventQueue.invokeLater(new Runnable(){
+            @Override
+            public void run(){
+                try{
+                    adjustTask(triggerEditor);
+                }finally{
+                    EditArray.this.onAdjusting = false;
+                }
+                return;
+            }
+        });
+
+        return;
+    }
+
+    /**
+     * エディタ間文字調整タスク本体。
+     * @param triggerEditor タスク実行のきっかけとなったエディタ
+     */
+    private void adjustTask(TalkEditor triggerEditor){
+        int initCaretPos = triggerEditor.getCaretPosition();
+
+        TalkEditor newFocus = null;
+        int newCaretPos = -1;
+
+        TalkEditor current = triggerEditor;
+        for(;;){
+            TalkEditor next;
+
+            if( ! isLastEditor(current) ){
+                next = nextEditor(current);
+                String nextContents = next.getText();
+                int nextLength = nextContents.length();
+
+                current.appendTail(nextContents);
+                String rest = current.chopRest();
+                int restLength;
+                if(rest == null) restLength = 0;
+                else             restLength = rest.length();
+
+                int chopLength = nextLength - restLength;
+                if(chopLength > 0){
+                    next.chopHead(chopLength);
+                }else if(chopLength < 0){
+                    rest = rest.substring(0, -chopLength);
+                    next.appendHead(rest);
+                }else{
+                    if(newFocus == null){
+                        newFocus = current;
+                        newCaretPos = initCaretPos;
+                    }
+                    break;
+                }
+            }else{
+                String rest = current.chopRest();
+                if(rest == null || this.editorList.size() >= MAX_EDITORS){
+                    if(newFocus == null){
+                        newFocus = current;
+                        if(current.getTextLength() >= initCaretPos){
+                            newCaretPos = initCaretPos;
+                        }else{
+                            newCaretPos = current.getTextLength();
+                        }
+                    }
+                    break;
+                }
+                next = nextEditor(current);
+                next.appendHead(rest);
+            }
+
+            if(newFocus == null){
+                int currentLength = current.getTextLength();
+                if(initCaretPos >= currentLength){
+                    initCaretPos -= currentLength;
+                }else{
+                    newFocus = current;
+                    newCaretPos = initCaretPos;
+                }
+            }
+
+            current = next;
+        }
+
+        if(newFocus != null){
+            newFocus.requestEditorFocus();
+            newFocus.setCaretPosition(newCaretPos);
+        }
+
+        adjustEditorsTail();
+
+        return;
+    }
+
+    /**
+     * エディタ集合末尾の空エディタを切り詰める。
+     * ただし最初のエディタ(通し番号1)は削除されない。
+     * フォーカスを持つエディタが削除された場合は、
+     * 削除されなかった最後のエディタにフォーカスが移る。
+     */
+    private void adjustEditorsTail(){
+        int editorNum = this.editorList.size();
+        if(editorNum <= 0) return;
+        TalkEditor lastEditor = this.editorList.get(editorNum - 1);
+
+        TalkEditor prevlostEditor = null;
+
+        boolean lostFocusedEditor = false;
+
+        for(;;){
+            int textLength = lastEditor.getTextLength();
+            int seqNo = lastEditor.getSequenceNumber();
+
+            if(lostFocusedEditor){
+                prevlostEditor = lastEditor;
+            }
+
+            if(textLength > 0) break;
+            if(seqNo <= 1) break;
+
+            if(lastEditor.hasEditorFocus()) lostFocusedEditor = true;
+            removeEditor(lastEditor);
+
+            lastEditor = prevEditor(lastEditor); // TODO ちょっと変
+        }
+
+        if(prevlostEditor != null){
+            int textLength = prevlostEditor.getTextLength();
+            prevlostEditor.requestEditorFocus();
+            prevlostEditor.setCaretPosition(textLength);
+        }
+
+        return;
+    }
+
+    /**
+     * フォーカスを持つエディタを取得する。
+     * @return エディタ
+     */
+    public TalkEditor getFocusedTalkEditor(){
+        for(TalkEditor editor : this.editorList){
+            if(editor.hasEditorFocus()) return editor;
+        }
+        return null;
+    }
+
+    /**
+     * フォーカスを持つエディタの次エディタがあればフォーカスを移し、
+     * カレット位置を0にする。
+     */
+    // TODO エディタのスクロール位置調整が必要。
+    public void forwardEditor(){
+        TalkEditor editor = getFocusedTalkEditor();
+        if(isLastEditor(editor)) return;
+        TalkEditor next = nextEditor(editor);
+        next.setCaretPosition(0);
+        next.requestEditorFocus();
+        return;
+    }
+
+    /**
+     * フォーカスを持つエディタの前エディタがあればフォーカスを移し、
+     * カレット位置を末尾に置く。
+     */
+    public void backwardEditor(){
+        TalkEditor editor = getFocusedTalkEditor();
+        TalkEditor prev = prevEditor(editor);
+        if(prev == null) return;
+        int length = prev.getTextLength();
+        prev.setCaretPosition(length);
+        prev.requestEditorFocus();
+        return;
+    }
+
+    /**
+     * 任意のエディタをアクティブにする。
+     * 同時にアクティブなエディタは一つのみ。
+     * @param editor アクティブにするエディタ
+     */
+    private void setActiveEditor(TalkEditor editor){
+        if(this.activeEditor != null){
+            this.activeEditor.setActive(false);
+        }
+
+        this.activeEditor = editor;
+
+        if(this.activeEditor != null){
+            this.activeEditor.setActive(true);
+        }
+
+        fireChangeActive();
+
+        return;
+    }
+
+    /**
+     * アクティブなエディタを返す。
+     * @return アクティブなエディタ。
+     */
+    public TalkEditor getActiveEditor(){
+        return this.activeEditor;
+    }
+
+    /**
+     * 全発言を連結した文字列を返す。
+     * @return 連結文字列
+     */
+    public CharSequence getAllText(){
+        StringBuilder result = new StringBuilder();
+
+        for(TalkEditor editor : this.editorList){
+            String text = editor.getText();
+            result.append(text);
+        }
+
+        return result;
+    }
+
+    /**
+     * 先頭エディタの0文字目から字を詰め込む。
+     * 2番目移行のエディタへはみ出すかもしれない。
+     * @param seq 詰め込む文字列
+     */
+    public void setAllText(CharSequence seq){
+        TalkEditor firstEditor = getTalkEditor(1);
+        Document doc = firstEditor.getDocument();
+        try{
+            doc.insertString(0, seq.toString(), null);
+        }catch(BadLocationException e){
+            assert false;
+        }
+        return;
+    }
+
+    /**
+     * 全エディタをクリアする。
+     */
+    public void clearAllEditor(){
+        int editorNum = this.editorList.size();
+        if(editorNum <= 0) return;
+
+        TalkEditor lastEditor = this.editorList.get(editorNum - 1);
+        for(;;){
+            removeEditor(lastEditor);
+            lastEditor = prevEditor(lastEditor);
+            if(lastEditor == null) break;
+        }
+
+        TalkEditor firstEditor = getTalkEditor(1);
+        firstEditor.clearText();
+        setActiveEditor(firstEditor);
+
+        return;
+    }
+
+    /**
+     * テキスト編集用フォントを指定する。
+     * @param textFont フォント
+     */
+    public void setTextFont(Font textFont){
+        this.textFont = textFont;
+        for(TalkEditor editor : this.editorList){
+            editor.setTextFont(this.textFont);
+            editor.repaint();
+        }
+        revalidate();
+        return;
+    }
+
+    /**
+     * テキスト編集用フォントを取得する。
+     * @return フォント
+     */
+    public Font getTextFont(){
+        return this.textFont;
+    }
+
+    /**
+     * アクティブエディタ変更通知用リスナの登録。
+     * @param listener リスナ
+     */
+    public void addChangeListener(ChangeListener listener){
+        this.listenerList.add(ChangeListener.class, listener);
+        return;
+    }
+
+    /**
+     * アクティブエディタ変更通知用リスナの削除。
+     * @param listener リスナ
+     */
+    public void removeChangeListener(ChangeListener listener){
+        this.listenerList.remove(ChangeListener.class, listener);
+        return;
+    }
+
+    /**
+     * アクティブエディタ変更通知を行う。
+     */
+    private void fireChangeActive(){
+        ChangeEvent event = new ChangeEvent(this);
+
+        ChangeListener[] listeners =
+                this.listenerList.getListeners(ChangeListener.class);
+        for(ChangeListener listener : listeners){
+            listener.stateChanged(event);
+        }
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     * エディタのフォーカス取得とともにアクティブ状態にする。
+     * @param event {@inheritDoc}
+     */
+    @Override
+    public void focusGained(FocusEvent event){
+        Object source = event.getSource();
+        if( ! (source instanceof JTextComponent) ) return;
+        JTextComponent textComp = (JTextComponent) source;
+
+        Document document = textComp.getDocument();
+        TalkEditor editor = getEditorFromDocument(document);
+
+        setActiveEditor(editor);
+
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @param event {@inheritDoc}
+     */
+    @Override
+    public void focusLost(FocusEvent event){
+        // NOTHING
+        return;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @return {@inheritDoc}
+     */
+    @Override
+    public Dimension getPreferredScrollableViewportSize(){
+        Dimension result = getPreferredSize();
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 横スクロールバーを極力出さないようレイアウトでがんばる。
+     * @return {@inheritDoc}
+     */
+    @Override
+    public boolean getScrollableTracksViewportWidth(){
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     * 縦スクロールバーを出しても良いのでレイアウトでがんばらない。
+     * @return {@inheritDoc}
+     */
+    @Override
+    public boolean getScrollableTracksViewportHeight(){
+        return false;
+    }
+
+    /**
+     *  {@inheritDoc}
+     * @param visibleRect {@inheritDoc}
+     * @param orientation {@inheritDoc}
+     * @param direction {@inheritDoc}
+     * @return {@inheritDoc}
+     */
+    @Override
+    public int getScrollableBlockIncrement(Rectangle visibleRect,
+                                           int orientation,
+                                           int direction ){
+        if(orientation == SwingConstants.VERTICAL){
+            return visibleRect.height;
+        }
+        return 10;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @param visibleRect {@inheritDoc}
+     * @param orientation {@inheritDoc}
+     * @param direction {@inheritDoc}
+     * @return {@inheritDoc}
+     */
+    @Override
+    public int getScrollableUnitIncrement(Rectangle visibleRect,
+                                          int orientation,
+                                          int direction ){
+        return 30; // TODO フォント高の1.5倍くらい?
+    }
+
+    /**
+     * エディタ内のカーソル移動を監視するための、
+     * カスタム化したナビゲーションフィルター。
+     * 必要に応じてエディタ間カーソル移動を行う。
+     */
+    private class CustomNavigation extends NavigationFilter{
+
+        /**
+         * コンストラクタ。
+         */
+        public CustomNavigation(){
+            super();
+            return;
+        }
+
+        /**
+         * {@inheritDoc}
+         * カーソル移動が行き詰まった場合、
+         * 隣接するエディタ間でカーソル移動を行う。
+         * @param text {@inheritDoc}
+         * @param pos {@inheritDoc}
+         * @param bias {@inheritDoc}
+         * @param direction {@inheritDoc}
+         * @param biasRet {@inheritDoc}
+         * @return {@inheritDoc}
+         * @throws javax.swing.text.BadLocationException {@inheritDoc}
+         */
+        @Override
+        public int getNextVisualPositionFrom(JTextComponent text,
+                                                 int pos,
+                                                 Bias bias,
+                                                 int direction,
+                                                 Bias[] biasRet )
+                                                 throws BadLocationException {
+            int result = super.getNextVisualPositionFrom(text,
+                                                         pos,
+                                                         bias,
+                                                         direction,
+                                                         biasRet );
+            if(result != pos) return result;
+
+            switch(direction){
+            case SwingConstants.WEST:
+            case SwingConstants.NORTH:
+                backwardEditor();
+                break;
+            case SwingConstants.EAST:
+            case SwingConstants.SOUTH:
+                forwardEditor();
+                break;
+            default:
+                assert false;
+            }
+
+            return result;
+        }
+    }
+
+    /**
+     * エディタの内容変更を監視し、随時エディタ間調整を行う。
+     */
+    private class DocWatcher implements DocumentListener{
+
+        /**
+         * コンストラクタ。
+         */
+        public DocWatcher(){
+            super();
+            return;
+        }
+
+        /**
+         * {@inheritDoc}
+         * @param event {@inheritDoc}
+         */
+        @Override
+        public void changedUpdate(DocumentEvent event){
+            detachAdjustTask(event);
+            return;
+        }
+
+        /**
+         * {@inheritDoc}
+         * @param event {@inheritDoc}
+         */
+        @Override
+        public void insertUpdate(DocumentEvent event){
+            detachAdjustTask(event);
+            return;
+        }
+
+        /**
+         * {@inheritDoc}
+         * @param event {@inheritDoc}
+         */
+        @Override
+        public void removeUpdate(DocumentEvent event){
+            detachAdjustTask(event);
+            return;
+        }
+    }
+
+}