--- /dev/null
+/*\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