OSDN Git Service

Merge commit '2458eff3aea04f67893bc824b5cf896fbb767332'
[jindolf/Jindolf.git] / src / main / java / jp / sourceforge / jindolf / Discussion.java
diff --git a/src/main/java/jp/sourceforge/jindolf/Discussion.java b/src/main/java/jp/sourceforge/jindolf/Discussion.java
new file mode 100644 (file)
index 0000000..9c83956
--- /dev/null
@@ -0,0 +1,1241 @@
+/*\r
+ * discussion viewer\r
+ *\r
+ * Copyright(c) 2008 olyutorskii\r
+ * $Id: Discussion.java 995 2010-03-15 03:54:09Z olyutorskii $\r
+ */\r
+\r
+package jp.sourceforge.jindolf;\r
+\r
+import java.awt.Color;\r
+import java.awt.Component;\r
+import java.awt.Dimension;\r
+import java.awt.Graphics;\r
+import java.awt.Graphics2D;\r
+import java.awt.Insets;\r
+import java.awt.Point;\r
+import java.awt.Rectangle;\r
+import java.awt.RenderingHints;\r
+import java.awt.event.ActionEvent;\r
+import java.awt.event.ActionListener;\r
+import java.awt.event.ComponentEvent;\r
+import java.awt.event.ComponentListener;\r
+import java.awt.event.MouseEvent;\r
+import java.awt.font.FontRenderContext;\r
+import java.io.IOException;\r
+import java.util.EventListener;\r
+import java.util.LinkedList;\r
+import java.util.List;\r
+import java.util.ListIterator;\r
+import java.util.regex.Pattern;\r
+import javax.swing.AbstractAction;\r
+import javax.swing.Action;\r
+import javax.swing.ActionMap;\r
+import javax.swing.InputMap;\r
+import javax.swing.JComponent;\r
+import javax.swing.JMenuItem;\r
+import javax.swing.JPopupMenu;\r
+import javax.swing.JTextField;\r
+import javax.swing.KeyStroke;\r
+import javax.swing.Scrollable;\r
+import javax.swing.SwingConstants;\r
+import javax.swing.event.EventListenerList;\r
+import javax.swing.event.MouseInputListener;\r
+import javax.swing.text.DefaultEditorKit;\r
+\r
+/**\r
+ * 発言表示画面。\r
+ *\r
+ * 表示に影響する要因は、Periodの中身、LayoutManagerによるサイズ変更、\r
+ * フォント属性の指定、フィルタリング操作、ドラッギングによる文字列選択操作、\r
+ * 文字列検索および検索ナビゲーション。\r
+ */\r
+@SuppressWarnings("serial")\r
+public class Discussion extends JComponent\r
+        implements Scrollable, MouseInputListener, ComponentListener{\r
+\r
+    private static final Color COLOR_NORMALBG = Color.BLACK;\r
+    private static final Color COLOR_SIMPLEBG = Color.WHITE;\r
+\r
+    private static final int MARGINTOP    =  50;\r
+    private static final int MARGINBOTTOM = 100;\r
+\r
+    private Period period;\r
+    private final List<TextRow> rowList       = new LinkedList<TextRow>();\r
+    private final List<TalkDraw> talkDrawList = new LinkedList<TalkDraw>();\r
+\r
+    private TopicFilter topicFilter;\r
+    private TopicFilter.FilterContext filterContext;\r
+    private RegexPattern regexPattern;\r
+\r
+    private Point dragFrom;\r
+\r
+    private FontInfo fontInfo;\r
+    private final RenderingHints hints = new RenderingHints(null);\r
+\r
+    private DialogPref dialogPref;\r
+\r
+    private Dimension idealSize;\r
+    private int lastWidth = -1;\r
+\r
+    private final DiscussionPopup popup = new DiscussionPopup();\r
+\r
+    private final EventListenerList thisListenerList =\r
+            new EventListenerList();\r
+\r
+    private final Action copySelectedAction =\r
+            new ProxyAction(ActionManager.CMD_COPY);\r
+\r
+    /**\r
+     * 発言表示画面を作成する。\r
+     */\r
+    public Discussion(){\r
+        super();\r
+\r
+        this.fontInfo = FontInfo.DEFAULT_FONTINFO;\r
+        this.dialogPref = new DialogPref();\r
+\r
+        this.hints.put(RenderingHints.KEY_ANTIALIASING,\r
+                       RenderingHints.VALUE_ANTIALIAS_ON);\r
+        this.hints.put(RenderingHints.KEY_RENDERING,\r
+                       RenderingHints.VALUE_RENDER_QUALITY);\r
+        updateRenderingHints();\r
+\r
+        setPeriod(null);\r
+\r
+        addMouseListener(this);\r
+        addMouseMotionListener(this);\r
+        addComponentListener(this);\r
+\r
+        setComponentPopupMenu(this.popup);\r
+\r
+        updateInputMap();\r
+        ActionMap actionMap = getActionMap();\r
+        actionMap.put(DefaultEditorKit.copyAction, this.copySelectedAction);\r
+\r
+        setColorDesign();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 描画設定の更新。\r
+     * FontRenderContextが更新された後は必ず呼び出す必要がある。\r
+     */\r
+    private void updateRenderingHints(){\r
+        Object textAliaseValue;\r
+        FontRenderContext context = this.fontInfo.getFontRenderContext();\r
+        if(context.isAntiAliased()){\r
+            textAliaseValue = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;\r
+        }else{\r
+            textAliaseValue = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;\r
+        }\r
+        this.hints.put(RenderingHints.KEY_TEXT_ANTIALIASING,\r
+                       textAliaseValue);\r
+\r
+        Object textFractionalValue;\r
+        if(context.usesFractionalMetrics()){\r
+            textFractionalValue = RenderingHints.VALUE_FRACTIONALMETRICS_ON;\r
+        }else{\r
+            textFractionalValue = RenderingHints.VALUE_FRACTIONALMETRICS_OFF;\r
+        }\r
+        this.hints.put(RenderingHints.KEY_FRACTIONALMETRICS,\r
+                       textFractionalValue);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 配色を設定する。\r
+     */\r
+    private void setColorDesign(){\r
+        Color fgColor;\r
+        if(this.dialogPref.isSimpleMode()){\r
+            fgColor = COLOR_SIMPLEBG;\r
+        }else{\r
+            fgColor = COLOR_NORMALBG;\r
+        }\r
+\r
+        setForeground(fgColor);\r
+        repaint();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * フォント描画設定を変更する。\r
+     * @param newFontInfo フォント設定\r
+     */\r
+    public void setFontInfo(FontInfo newFontInfo){\r
+        this.fontInfo = newFontInfo;\r
+\r
+        updateRenderingHints();\r
+\r
+        for(TextRow row : this.rowList){\r
+            row.setFontInfo(this.fontInfo);\r
+        }\r
+\r
+        setColorDesign();\r
+        layoutRows();\r
+\r
+        revalidate();\r
+        repaint();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 発言表示設定を変更する。\r
+     * @param newPref 発言表示設定\r
+     */\r
+    public void setDialogPref(DialogPref newPref){\r
+        this.dialogPref = newPref;\r
+\r
+        for(TextRow row : this.rowList){\r
+            if(row instanceof TalkDraw){\r
+                TalkDraw talkDraw = (TalkDraw) row;\r
+                talkDraw.setDialogPref(this.dialogPref);\r
+            }else if(row instanceof SysEventDraw){\r
+                SysEventDraw sysDraw = (SysEventDraw) row;\r
+                sysDraw.setDialogPref(this.dialogPref);\r
+            }\r
+        }\r
+\r
+        setColorDesign();\r
+        layoutRows();\r
+\r
+        revalidate();\r
+        repaint();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 現在のPeriodを返す。\r
+     * @return 現在のPeriod\r
+     */\r
+    public Period getPeriod(){\r
+        return this.period;\r
+    }\r
+\r
+    /**\r
+     * Periodを更新する。\r
+     * 新しいPeriodの表示内容はまだ反映されない。\r
+     * @param period 新しいPeriod\r
+     */\r
+    public final void setPeriod(Period period){\r
+        if(period == null){\r
+            this.period = null;\r
+            this.rowList.clear();\r
+            this.talkDrawList.clear();\r
+            return;\r
+        }\r
+\r
+        if(   this.period == period\r
+           && period.getTopics() == this.rowList.size() ){\r
+            filterTopics();\r
+            return;\r
+        }\r
+\r
+        this.period = period;\r
+\r
+        this.filterContext = null;\r
+\r
+        this.rowList.clear();\r
+        this.talkDrawList.clear();\r
+        for(Topic topic : this.period.getTopicList()){\r
+            TextRow row;\r
+            if(topic instanceof Talk){\r
+                Talk talk = (Talk) topic;\r
+                TalkDraw talkDraw = new TalkDraw(talk,\r
+                                                 this.dialogPref,\r
+                                                 this.fontInfo );\r
+                this.talkDrawList.add(talkDraw);\r
+                row = talkDraw;\r
+            }else if(topic instanceof SysEvent){\r
+                SysEvent sysEvent = (SysEvent) topic;\r
+                row = new SysEventDraw(sysEvent,\r
+                                       this.dialogPref,\r
+                                       this.fontInfo );\r
+            }else{\r
+                assert false;\r
+                continue;\r
+            }\r
+            this.rowList.add(row);\r
+        }\r
+\r
+        filterTopics();\r
+\r
+        clearSizeCache();\r
+\r
+        layoutRows();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 発言フィルタを設定する。\r
+     * @param filter 発言フィルタ\r
+     */\r
+    public void setTopicFilter(TopicFilter filter){\r
+        this.topicFilter = filter;\r
+        filtering();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 発言フィルタを適用する。\r
+     */\r
+    public void filtering(){\r
+        if(   this.topicFilter != null\r
+           && this.topicFilter.isSame(this.filterContext)){\r
+            return;\r
+        }\r
+\r
+        if(this.topicFilter != null){\r
+            this.filterContext = this.topicFilter.getFilterContext();\r
+        }else{\r
+            this.filterContext = null;\r
+        }\r
+\r
+        filterTopics();\r
+        layoutVertical();\r
+\r
+        clearSelect();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 検索パターンを取得する。\r
+     * @return 検索パターン\r
+     */\r
+    public RegexPattern getRegexPattern(){\r
+        return this.regexPattern;\r
+    }\r
+\r
+    /**\r
+     * 与えられた正規表現にマッチする文字列をハイライト描画する。\r
+     * @param newPattern 検索パターン\r
+     * @return ヒット件数\r
+     */\r
+    public int setRegexPattern(RegexPattern newPattern){\r
+        this.regexPattern = newPattern;\r
+\r
+        int total = 0;\r
+\r
+        clearHotTarget();\r
+\r
+        Pattern pattern = null;\r
+        if(this.regexPattern != null){\r
+            pattern = this.regexPattern.getPattern();\r
+        }\r
+\r
+        for(TalkDraw talkDraw : this.talkDrawList){\r
+            total += talkDraw.setRegex(pattern);\r
+        }\r
+\r
+        repaint();\r
+\r
+        return total;\r
+    }\r
+\r
+    /**\r
+     * 検索結果の次候補をハイライト表示する。\r
+     */\r
+    public void nextHotTarget(){\r
+        TalkDraw oldTalk = null;\r
+        int oldIndex = -1;\r
+        TalkDraw newTalk = null;\r
+        int newIndex = -1;\r
+        TalkDraw firstTalk = null;\r
+\r
+        boolean findOld = true;\r
+        for(TalkDraw talkDraw : this.talkDrawList){\r
+            int matches = talkDraw.getRegexMatches();\r
+            if(firstTalk == null && matches > 0){\r
+                firstTalk = talkDraw;\r
+            }\r
+            if(findOld){\r
+                int index = talkDraw.getHotTargetIndex();\r
+                if(index < 0) continue;\r
+                oldTalk = talkDraw;\r
+                oldIndex = index;\r
+                scrollRectWithMargin(talkDraw.getHotTargetRectangle());\r
+                if(oldIndex < matches - 1 && ! isFiltered(talkDraw) ){\r
+                    newTalk = talkDraw;\r
+                    newIndex = oldIndex + 1;\r
+                    break;\r
+                }\r
+                findOld = false;\r
+            }else{\r
+                if(isFiltered(talkDraw)) continue;\r
+                if(matches <= 0) continue;\r
+                newTalk = talkDraw;\r
+                newIndex = 0;\r
+                break;\r
+            }\r
+        }\r
+\r
+        Rectangle showRect = null;\r
+        if(oldTalk == null && firstTalk != null){\r
+            firstTalk.setHotTargetIndex(0);\r
+            showRect = firstTalk.getHotTargetRectangle();\r
+        }else if(   oldTalk != null\r
+                 && newTalk != null){\r
+            oldTalk.clearHotTarget();\r
+            newTalk.setHotTargetIndex(newIndex);\r
+            showRect = newTalk.getHotTargetRectangle();\r
+        }\r
+\r
+        if(showRect != null){\r
+            scrollRectWithMargin(showRect);\r
+        }\r
+\r
+        repaint();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 検索結果の前候補をハイライト表示する。\r
+     */\r
+    public void prevHotTarget(){\r
+        TalkDraw oldTalk = null;\r
+        int oldIndex = -1;\r
+        TalkDraw newTalk = null;\r
+        int newIndex = -1;\r
+        TalkDraw firstTalk = null;\r
+\r
+        boolean findOld = true;\r
+        int size = this.talkDrawList.size();\r
+        ListIterator<TalkDraw> iterator =\r
+                this.talkDrawList.listIterator(size);\r
+        while(iterator.hasPrevious()){\r
+            TalkDraw talkDraw = iterator.previous();\r
+            int matches = talkDraw.getRegexMatches();\r
+            if(firstTalk == null && matches > 0){\r
+                firstTalk = talkDraw;\r
+            }\r
+            if(findOld){\r
+                int index = talkDraw.getHotTargetIndex();\r
+                if(index < 0) continue;\r
+                oldTalk = talkDraw;\r
+                oldIndex = index;\r
+                scrollRectWithMargin(talkDraw.getHotTargetRectangle());\r
+                if(oldIndex > 0 && ! isFiltered(talkDraw) ){\r
+                    newTalk = talkDraw;\r
+                    newIndex = oldIndex - 1;\r
+                    break;\r
+                }\r
+                findOld = false;\r
+            }else{\r
+                if(isFiltered(talkDraw)) continue;\r
+                if(matches <= 0) continue;\r
+                newTalk = talkDraw;\r
+                newIndex = matches - 1;\r
+                break;\r
+            }\r
+        }\r
+\r
+        Rectangle showRect = null;\r
+        if(oldTalk == null && firstTalk != null){\r
+            int matches = firstTalk.getRegexMatches();\r
+            firstTalk.setHotTargetIndex(matches - 1);\r
+            showRect = firstTalk.getHotTargetRectangle();\r
+        }else if(   oldTalk != null\r
+                 && newTalk != null){\r
+            oldTalk.clearHotTarget();\r
+            newTalk.setHotTargetIndex(newIndex);\r
+            showRect = newTalk.getHotTargetRectangle();\r
+        }\r
+\r
+        if(showRect != null){\r
+            scrollRectWithMargin(showRect);\r
+        }\r
+\r
+        repaint();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 検索結果の特殊ハイライト表示を解除。\r
+     */\r
+    public void clearHotTarget(){\r
+        for(TalkDraw talkDraw : this.talkDrawList){\r
+            talkDraw.clearHotTarget();\r
+        }\r
+        repaint();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 指定した領域に若干の上下マージンを付けて\r
+     * スクロールウィンドウに表示させる。\r
+     * @param rectangle 指定領域\r
+     */\r
+    private void scrollRectWithMargin(Rectangle rectangle){\r
+        Rectangle show = new Rectangle(rectangle);\r
+        show.y      -= MARGINTOP;\r
+        show.height += MARGINTOP + MARGINBOTTOM;\r
+\r
+        scrollRectToVisible(show);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 過去に計算した寸法を破棄する。\r
+     */\r
+    private void clearSizeCache(){\r
+        this.idealSize = null;\r
+        this.lastWidth = -1;\r
+        revalidate();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 指定した矩形がフィルタリング対象か判定する。\r
+     * @param row 矩形\r
+     * @return フィルタリング対象ならtrue\r
+     */\r
+    private boolean isFiltered(TextRow row){\r
+        if(this.topicFilter == null) return false;\r
+\r
+        Topic topic;\r
+        if(row instanceof TalkDraw){\r
+            topic = ((TalkDraw)row).getTalk();\r
+        }else if(row instanceof SysEventDraw){\r
+            topic = ((SysEventDraw)row).getSysEvent();\r
+        }else{\r
+            return false;\r
+        }\r
+\r
+        return this.topicFilter.isFiltered(topic);\r
+    }\r
+\r
+    /**\r
+     * フィルタリング指定に従いTextRowを表示するか否か設定する。\r
+     */\r
+    private void filterTopics(){\r
+        for(TextRow row : this.rowList){\r
+            if(isFiltered(row)) row.setVisible(false);\r
+            else                row.setVisible(true);\r
+        }\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 幅を設定する。\r
+     * 全子TextRowがリサイズされる。\r
+     * @param width コンポーネント幅\r
+     */\r
+    private void setWidth(int width){\r
+        this.lastWidth = width;\r
+        Insets insets = getInsets();\r
+        int rowWidth = width - (insets.left + insets.right);\r
+        for(TextRow row : this.rowList){\r
+            row.setWidth(rowWidth);\r
+        }\r
+\r
+        layoutVertical();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 子TextRowの縦位置レイアウトを行う。\r
+     * フィルタリングが反映される。\r
+     * TextRowは必要に応じて移動させられるがリサイズされることはない。\r
+     */\r
+    private void layoutVertical(){\r
+        Rectangle unionRect = null;\r
+        Insets insets = getInsets();\r
+        int vertPos = insets.top;\r
+\r
+        for(TextRow row : this.rowList){\r
+            if( ! row.isVisible() ) continue;\r
+\r
+            row.setPos(insets.left, vertPos);\r
+            Rectangle rowBound = row.getBounds();\r
+            vertPos += rowBound.height;\r
+\r
+            if(unionRect == null){\r
+                unionRect = new Rectangle(rowBound);\r
+            }else{\r
+                unionRect.add(rowBound);\r
+            }\r
+        }\r
+\r
+        if(unionRect == null){\r
+            unionRect = new Rectangle(insets.left, insets.top, 0, 0);\r
+        }\r
+\r
+        if(this.idealSize == null){\r
+            this.idealSize = new Dimension();\r
+        }\r
+\r
+        int newWidth  = insets.left + unionRect.width  + insets.right;\r
+        int newHeight = insets.top  + unionRect.height + insets.bottom;\r
+\r
+        this.idealSize.setSize(newWidth, newHeight);\r
+\r
+        setPreferredSize(this.idealSize);\r
+\r
+        revalidate();\r
+        repaint();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * Rowsの縦位置を再レイアウトする。\r
+     */\r
+    public void layoutRows(){\r
+        int width = getWidth();\r
+        setWidth(width);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param g {@inheritDoc}\r
+     */\r
+    @Override\r
+    public void paintComponent(Graphics g){\r
+        Graphics2D g2 = (Graphics2D) g;\r
+        g2.setRenderingHints(this.hints);\r
+\r
+        Rectangle clipRect = g2.getClipBounds();\r
+        g2.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);\r
+\r
+        for(TextRow row : this.rowList){\r
+            if( ! row.isVisible() ) continue;\r
+\r
+            Rectangle rowRect = row.getBounds();\r
+            if( ! rowRect.intersects(clipRect) ) continue;\r
+\r
+            row.paint(g2);\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    public Dimension getPreferredScrollableViewportSize(){\r
+        return getPreferredSize();\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    public boolean getScrollableTracksViewportWidth(){\r
+        return true;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\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 30; // TODO フォント高 × 1.5 ぐらい?\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;\r
+    }\r
+\r
+    /**\r
+     * 任意の発言の表示が占める画面領域を返す。\r
+     * 発言がフィルタリング対象の時はnullを返す。\r
+     * @param talk 発言\r
+     * @return 領域\r
+     */\r
+    public Rectangle getTalkBounds(Talk talk){\r
+        if(   this.topicFilter != null\r
+           && this.topicFilter.isFiltered(talk)) return null;\r
+\r
+        for(TalkDraw talkDraw : this.talkDrawList){\r
+            if(talkDraw.getTalk() == talk){\r
+                Rectangle rect = talkDraw.getBounds();\r
+                return rect;\r
+            }\r
+        }\r
+\r
+        return null;\r
+    }\r
+\r
+    /**\r
+     * ドラッグ処理を行う。\r
+     * @param from ドラッグ開始位置\r
+     * @param to 現在のドラッグ位置\r
+     */\r
+    private void drag(Point from, Point to){\r
+        Rectangle dragRegion = new Rectangle();\r
+        dragRegion.setFrameFromDiagonal(from, to);\r
+\r
+        for(TextRow row : this.rowList){\r
+            if(isFiltered(row)) continue;\r
+            if( ! row.getBounds().intersects(dragRegion) ) continue;\r
+            row.drag(from, to);\r
+        }\r
+        repaint();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 選択範囲の解除。\r
+     */\r
+    private void clearSelect(){\r
+        for(TextRow row : this.rowList){\r
+            row.clearSelect();\r
+        }\r
+        repaint();\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 与えられた点座標を包含する発言を返す。\r
+     * @param pt 点座標(JComponent基準)\r
+     * @return 点座標を含む発言。含む発言がなければnullを返す。\r
+     */\r
+    // TODO 二分探索とかしたい。\r
+    private TalkDraw getHittedTalkDraw(Point pt){\r
+        for(TalkDraw talkDraw : this.talkDrawList){\r
+            if(isFiltered(talkDraw)) continue;\r
+            Rectangle bounds = talkDraw.getBounds();\r
+            if(bounds.contains(pt)) return talkDraw;\r
+        }\r
+        return null;\r
+    }\r
+\r
+    /**\r
+     * アンカークリック動作の処理。\r
+     * @param pt クリックポイント\r
+     */\r
+    private void hitAnchor(Point pt){\r
+        TalkDraw talkDraw = getHittedTalkDraw(pt);\r
+        if(talkDraw == null) return;\r
+\r
+        Anchor anchor = talkDraw.getAnchor(pt);\r
+        if(anchor == null) return;\r
+\r
+        for(AnchorHitListener listener : getAnchorHitListeners()){\r
+            AnchorHitEvent event =\r
+                    new AnchorHitEvent(this, talkDraw, anchor, pt);\r
+            listener.anchorHitted(event);\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 検索マッチ文字列クリック動作の処理。\r
+     * @param pt クリックポイント\r
+     */\r
+    private void hitRegex(Point pt){\r
+        TalkDraw talkDraw = getHittedTalkDraw(pt);\r
+        if(talkDraw == null) return;\r
+\r
+        int index = talkDraw.getRegexMatchIndex(pt);\r
+        if(index < 0) return;\r
+\r
+        clearHotTarget();\r
+        talkDraw.setHotTargetIndex(index);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * アンカーヒット処理を行う。\r
+     * MouseInputListenerを参照せよ。\r
+     * @param event {@inheritDoc}\r
+     */\r
+    // TODO 距離判定がシビアすぎ\r
+    public void mouseClicked(MouseEvent event){\r
+        Point pt = event.getPoint();\r
+        if(event.getButton() == MouseEvent.BUTTON1){\r
+            clearSelect();\r
+            hitAnchor(pt);\r
+            hitRegex(pt);\r
+        }\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param event {@inheritDoc}\r
+     */\r
+    public void mouseEntered(MouseEvent event){\r
+        // TODO ここでキーボードフォーカス処理が必要?\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param event {@inheritDoc}\r
+     */\r
+    public void mouseExited(MouseEvent event){\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * ドラッグ開始処理を行う。\r
+     * @param event {@inheritDoc}\r
+     */\r
+    public void mousePressed(MouseEvent event){\r
+        requestFocusInWindow();\r
+\r
+        if(event.getButton() == MouseEvent.BUTTON1){\r
+            clearSelect();\r
+            this.dragFrom = event.getPoint();\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * ドラッグ終了処理を行う。\r
+     * @param event {@inheritDoc}\r
+     */\r
+    public void mouseReleased(MouseEvent event){\r
+        if(event.getButton() == MouseEvent.BUTTON1){\r
+            this.dragFrom = null;\r
+        }\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * ドラッグ処理を行う。\r
+     * @param event {@inheritDoc}\r
+     */\r
+    // TODO ドラッグ範囲がビューポートを超えたら自動的にスクロールしてほしい。\r
+    public void mouseDragged(MouseEvent event){\r
+        if(this.dragFrom == null) return;\r
+        Point dragTo = event.getPoint();\r
+        drag(this.dragFrom, dragTo);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param event {@inheritDoc}\r
+     */\r
+    public void mouseMoved(MouseEvent event){\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param event {@inheritDoc}\r
+     */\r
+    public void componentShown(ComponentEvent event){\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param event {@inheritDoc}\r
+     */\r
+    public void componentHidden(ComponentEvent event){\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param event {@inheritDoc}\r
+     */\r
+    public void componentMoved(ComponentEvent event){\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param event {@inheritDoc}\r
+     */\r
+    public void componentResized(ComponentEvent event){\r
+        int width  = getWidth();\r
+        int height = getHeight();\r
+        if(width != this.lastWidth){\r
+            setWidth(width);\r
+        }\r
+        if(   this.idealSize.width != width\r
+           || this.idealSize.height != height ){\r
+            revalidate();\r
+        }\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * 選択文字列を返す。\r
+     * @return 選択文字列\r
+     */\r
+    public CharSequence getSelected(){\r
+        StringBuilder selected = new StringBuilder();\r
+\r
+        for(TextRow row : this.rowList){\r
+            if(isFiltered(row)) continue;\r
+            try{\r
+                row.appendSelected(selected);\r
+            }catch(IOException e){\r
+                assert false; // ありえない\r
+                return null;\r
+            }\r
+        }\r
+\r
+        if(selected.length() <= 0) return null;\r
+\r
+        return selected;\r
+    }\r
+\r
+    /**\r
+     * 選択文字列をクリップボードにコピーする。\r
+     * @return 選択文字列\r
+     */\r
+    public CharSequence copySelected(){\r
+        CharSequence selected = getSelected();\r
+        if(selected == null) return null;\r
+        ClipboardAction.copyToClipboard(selected);\r
+        return selected;\r
+    }\r
+\r
+    /**\r
+     * 矩形の示す一発言をクリップボードにコピーする。\r
+     * @return コピーした文字列\r
+     */\r
+    public CharSequence copyTalk(){\r
+        TalkDraw talkDraw = this.popup.lastPopupedTalkDraw;\r
+        if(talkDraw == null) return null;\r
+        Talk talk = talkDraw.getTalk();\r
+\r
+        StringBuilder selected = new StringBuilder();\r
+\r
+        Avatar avatar = talk.getAvatar();\r
+        selected.append(avatar.getName()).append(' ');\r
+\r
+        String anchor = talk.getAnchorNotation();\r
+        selected.append(anchor);\r
+        if(talk.hasTalkNo()){\r
+            selected.append(' ').append(talk.getAnchorNotation_G());\r
+        }\r
+        selected.append('\n');\r
+\r
+        selected.append(talk.getDialog());\r
+        if(selected.charAt(selected.length() - 1) != '\n'){\r
+            selected.append('\n');\r
+        }\r
+\r
+        ClipboardAction.copyToClipboard(selected);\r
+\r
+        return selected;\r
+    }\r
+\r
+    /**\r
+     * ポップアップメニュートリガ座標に発言があればそれを返す。\r
+     * @return 発言\r
+     */\r
+    public Talk getPopupedTalk(){\r
+        TalkDraw talkDraw = this.popup.lastPopupedTalkDraw;\r
+        if(talkDraw == null) return null;\r
+        Talk talk = talkDraw.getTalk();\r
+        return talk;\r
+    }\r
+\r
+    /**\r
+     * ポップアップメニュートリガ座標にアンカーがあればそれを返す。\r
+     * @return アンカー\r
+     */\r
+    public Anchor getPopupedAnchor(){\r
+        return this.popup.lastPopupedAnchor;\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public void updateUI(){\r
+        super.updateUI();\r
+        this.popup.updateUI();\r
+\r
+        updateInputMap();\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * COPY処理を行うキーの設定をJTextFieldから流用する。\r
+     * おそらくはCtrl-C。MacならCommand-Cかも。\r
+     */\r
+    private void updateInputMap(){\r
+        InputMap thisInputMap = getInputMap();\r
+\r
+        InputMap sampleInputMap;\r
+        sampleInputMap = new JTextField().getInputMap();\r
+        KeyStroke[] strokes = sampleInputMap.allKeys();\r
+        for(KeyStroke stroke : strokes){\r
+            Object bind = sampleInputMap.get(stroke);\r
+            if(bind.equals(DefaultEditorKit.copyAction)){\r
+                thisInputMap.put(stroke, DefaultEditorKit.copyAction);\r
+            }\r
+        }\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * ActionListenerを追加する。\r
+     * @param listener リスナー\r
+     */\r
+    public void addActionListener(ActionListener listener){\r
+        this.thisListenerList.add(ActionListener.class, listener);\r
+\r
+        this.popup.menuCopy       .addActionListener(listener);\r
+        this.popup.menuSelTalk    .addActionListener(listener);\r
+        this.popup.menuJumpAnchor .addActionListener(listener);\r
+        this.popup.menuWebTalk    .addActionListener(listener);\r
+        this.popup.menuSummary    .addActionListener(listener);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * ActionListenerを削除する。\r
+     * @param listener リスナー\r
+     */\r
+    public void removeActionListener(ActionListener listener){\r
+        this.thisListenerList.remove(ActionListener.class, listener);\r
+\r
+        this.popup.menuCopy       .removeActionListener(listener);\r
+        this.popup.menuSelTalk    .removeActionListener(listener);\r
+        this.popup.menuJumpAnchor .removeActionListener(listener);\r
+        this.popup.menuWebTalk    .removeActionListener(listener);\r
+        this.popup.menuSummary    .removeActionListener(listener);\r
+\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * ActionListenerを列挙する。\r
+     * @return すべてのActionListener\r
+     */\r
+    public ActionListener[] getActionListeners(){\r
+        return this.thisListenerList.getListeners(ActionListener.class);\r
+    }\r
+\r
+    /**\r
+     * AnchorHitListenerを追加する。\r
+     * @param listener リスナー\r
+     */\r
+    public void addAnchorHitListener(AnchorHitListener listener){\r
+        this.thisListenerList.add(AnchorHitListener.class, listener);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * AnchorHitListenerを削除する。\r
+     * @param listener リスナー\r
+     */\r
+    public void removeAnchorHitListener(AnchorHitListener listener){\r
+        this.thisListenerList.remove(AnchorHitListener.class, listener);\r
+        return;\r
+    }\r
+\r
+    /**\r
+     * AnchorHitListenerを列挙する。\r
+     * @return すべてのAnchorHitListener\r
+     */\r
+    public AnchorHitListener[] getAnchorHitListeners(){\r
+        return this.thisListenerList.getListeners(AnchorHitListener.class);\r
+    }\r
+\r
+    /**\r
+     * {@inheritDoc}\r
+     * @param <T> {@inheritDoc}\r
+     * @param listenerType {@inheritDoc}\r
+     * @return {@inheritDoc}\r
+     */\r
+    @Override\r
+    public <T extends EventListener> T[] getListeners(Class<T> listenerType){\r
+        T[] result;\r
+        result = this.thisListenerList.getListeners(listenerType);\r
+\r
+        if(result.length <= 0){\r
+            result = super.getListeners(listenerType);\r
+        }\r
+\r
+        return result;\r
+    }\r
+\r
+    /**\r
+     * キーボード入力用ダミーAction。\r
+     */\r
+    private class ProxyAction extends AbstractAction{\r
+\r
+        private final String command;\r
+\r
+        /**\r
+         * コンストラクタ。\r
+         * @param command コマンド\r
+         * @throws NullPointerException 引数がnull\r
+         */\r
+        public ProxyAction(String command) throws NullPointerException{\r
+            super();\r
+            if(command == null) throw new NullPointerException();\r
+            this.command = command;\r
+            return;\r
+        }\r
+\r
+        /**\r
+         * {@inheritDoc}\r
+         * @param event {@inheritDoc}\r
+         */\r
+        public void actionPerformed(ActionEvent event){\r
+            Object source  = event.getSource();\r
+            int id         = event.getID();\r
+            String actcmd  = this.command;\r
+            long when      = event.getWhen();\r
+            int modifiers  = event.getModifiers();\r
+\r
+            for(ActionListener listener : getActionListeners()){\r
+                ActionEvent newEvent = new ActionEvent(source,\r
+                                                       id,\r
+                                                       actcmd,\r
+                                                       when,\r
+                                                       modifiers );\r
+                listener.actionPerformed(newEvent);\r
+            }\r
+\r
+            return;\r
+        }\r
+    };\r
+\r
+    /**\r
+     * ポップアップメニュー。\r
+     */\r
+    private class DiscussionPopup extends JPopupMenu{\r
+\r
+        private final JMenuItem menuCopy =\r
+                new JMenuItem("選択範囲をコピー");\r
+        private final JMenuItem menuSelTalk =\r
+                new JMenuItem("この発言をコピー");\r
+        private final JMenuItem menuJumpAnchor =\r
+                new JMenuItem("アンカーの示す先へジャンプ");\r
+        private final JMenuItem menuWebTalk =\r
+                new JMenuItem("この発言をブラウザで表示...");\r
+        private final JMenuItem menuSummary =\r
+                new JMenuItem("発言を集計...");\r
+\r
+        private TalkDraw lastPopupedTalkDraw;\r
+        private Anchor lastPopupedAnchor;\r
+\r
+        /**\r
+         * コンストラクタ。\r
+         */\r
+        public DiscussionPopup(){\r
+            super();\r
+\r
+            add(this.menuCopy);\r
+            add(this.menuSelTalk);\r
+            addSeparator();\r
+            add(this.menuJumpAnchor);\r
+            add(this.menuWebTalk);\r
+            addSeparator();\r
+            add(this.menuSummary);\r
+\r
+            this.menuCopy\r
+                .setActionCommand(ActionManager.CMD_COPY);\r
+            this.menuSelTalk\r
+                .setActionCommand(ActionManager.CMD_COPYTALK);\r
+            this.menuJumpAnchor\r
+                .setActionCommand(ActionManager.CMD_JUMPANCHOR);\r
+            this.menuWebTalk\r
+                .setActionCommand(ActionManager.CMD_WEBTALK);\r
+            this.menuSummary\r
+                .setActionCommand(ActionManager.CMD_DAYSUMMARY);\r
+\r
+            this.menuWebTalk.setIcon(GUIUtils.getWWWIcon());\r
+\r
+            return;\r
+        }\r
+\r
+        /**\r
+         * {@inheritDoc}\r
+         * @param comp {@inheritDoc}\r
+         * @param x {@inheritDoc}\r
+         * @param y {@inheritDoc}\r
+         */\r
+        @Override\r
+        public void show(Component comp, int x, int y){\r
+            Point point = new Point(x, y);\r
+\r
+            this.lastPopupedTalkDraw = getHittedTalkDraw(point);\r
+            if(this.lastPopupedTalkDraw != null){\r
+                this.menuSelTalk.setEnabled(true);\r
+                this.menuWebTalk.setEnabled(true);\r
+            }else{\r
+                this.menuSelTalk.setEnabled(false);\r
+                this.menuWebTalk.setEnabled(false);\r
+            }\r
+\r
+            if(this.lastPopupedTalkDraw != null){\r
+                this.lastPopupedAnchor =\r
+                        this.lastPopupedTalkDraw.getAnchor(point);\r
+            }else{\r
+                this.lastPopupedAnchor = null;\r
+            }\r
+\r
+            if(this.lastPopupedAnchor != null){\r
+                this.menuJumpAnchor.setEnabled(true);\r
+            }else{\r
+                this.menuJumpAnchor.setEnabled(false);\r
+            }\r
+\r
+            if(getSelected() != null){\r
+                this.menuCopy.setEnabled(true);\r
+            }else{\r
+                this.menuCopy.setEnabled(false);\r
+            }\r
+\r
+            super.show(comp, x, y);\r
+\r
+            return;\r
+        }\r
+    }\r
+\r
+    // TODO シンプルモードの追加\r
+    // Period変更を追跡するリスナ化\r
+}\r