OSDN Git Service

@Override追加
[jindolf/Jindolf.git] / src / main / java / jp / sourceforge / jindolf / Discussion.java
1 /*\r
2  * discussion viewer\r
3  *\r
4  * License : The MIT License\r
5  * Copyright(c) 2008 olyutorskii\r
6  */\r
7 \r
8 package jp.sourceforge.jindolf;\r
9 \r
10 import java.awt.Color;\r
11 import java.awt.Component;\r
12 import java.awt.Dimension;\r
13 import java.awt.Graphics;\r
14 import java.awt.Graphics2D;\r
15 import java.awt.Insets;\r
16 import java.awt.Point;\r
17 import java.awt.Rectangle;\r
18 import java.awt.RenderingHints;\r
19 import java.awt.event.ActionEvent;\r
20 import java.awt.event.ActionListener;\r
21 import java.awt.event.ComponentEvent;\r
22 import java.awt.event.ComponentListener;\r
23 import java.awt.event.MouseEvent;\r
24 import java.awt.font.FontRenderContext;\r
25 import java.io.IOException;\r
26 import java.util.EventListener;\r
27 import java.util.LinkedList;\r
28 import java.util.List;\r
29 import java.util.ListIterator;\r
30 import java.util.regex.Pattern;\r
31 import javax.swing.AbstractAction;\r
32 import javax.swing.Action;\r
33 import javax.swing.ActionMap;\r
34 import javax.swing.InputMap;\r
35 import javax.swing.JComponent;\r
36 import javax.swing.JMenuItem;\r
37 import javax.swing.JPopupMenu;\r
38 import javax.swing.JTextField;\r
39 import javax.swing.KeyStroke;\r
40 import javax.swing.Scrollable;\r
41 import javax.swing.SwingConstants;\r
42 import javax.swing.event.EventListenerList;\r
43 import javax.swing.event.MouseInputListener;\r
44 import javax.swing.text.DefaultEditorKit;\r
45 \r
46 /**\r
47  * 発言表示画面。\r
48  *\r
49  * 表示に影響する要因は、Periodの中身、LayoutManagerによるサイズ変更、\r
50  * フォント属性の指定、フィルタリング操作、ドラッギングによる文字列選択操作、\r
51  * 文字列検索および検索ナビゲーション。\r
52  */\r
53 @SuppressWarnings("serial")\r
54 public class Discussion extends JComponent\r
55         implements Scrollable, MouseInputListener, ComponentListener{\r
56 \r
57     private static final Color COLOR_NORMALBG = Color.BLACK;\r
58     private static final Color COLOR_SIMPLEBG = Color.WHITE;\r
59 \r
60     private static final int MARGINTOP    =  50;\r
61     private static final int MARGINBOTTOM = 100;\r
62 \r
63     private Period period;\r
64     private final List<TextRow> rowList       = new LinkedList<TextRow>();\r
65     private final List<TalkDraw> talkDrawList = new LinkedList<TalkDraw>();\r
66 \r
67     private TopicFilter topicFilter;\r
68     private TopicFilter.FilterContext filterContext;\r
69     private RegexPattern regexPattern;\r
70 \r
71     private Point dragFrom;\r
72 \r
73     private FontInfo fontInfo;\r
74     private final RenderingHints hints = new RenderingHints(null);\r
75 \r
76     private DialogPref dialogPref;\r
77 \r
78     private Dimension idealSize;\r
79     private int lastWidth = -1;\r
80 \r
81     private final DiscussionPopup popup = new DiscussionPopup();\r
82 \r
83     private final EventListenerList thisListenerList =\r
84             new EventListenerList();\r
85 \r
86     private final Action copySelectedAction =\r
87             new ProxyAction(ActionManager.CMD_COPY);\r
88 \r
89     /**\r
90      * 発言表示画面を作成する。\r
91      */\r
92     public Discussion(){\r
93         super();\r
94 \r
95         this.fontInfo = FontInfo.DEFAULT_FONTINFO;\r
96         this.dialogPref = new DialogPref();\r
97 \r
98         this.hints.put(RenderingHints.KEY_ANTIALIASING,\r
99                        RenderingHints.VALUE_ANTIALIAS_ON);\r
100         this.hints.put(RenderingHints.KEY_RENDERING,\r
101                        RenderingHints.VALUE_RENDER_QUALITY);\r
102         updateRenderingHints();\r
103 \r
104         setPeriod(null);\r
105 \r
106         addMouseListener(this);\r
107         addMouseMotionListener(this);\r
108         addComponentListener(this);\r
109 \r
110         setComponentPopupMenu(this.popup);\r
111 \r
112         updateInputMap();\r
113         ActionMap actionMap = getActionMap();\r
114         actionMap.put(DefaultEditorKit.copyAction, this.copySelectedAction);\r
115 \r
116         setColorDesign();\r
117 \r
118         return;\r
119     }\r
120 \r
121     /**\r
122      * 描画設定の更新。\r
123      * FontRenderContextが更新された後は必ず呼び出す必要がある。\r
124      */\r
125     private void updateRenderingHints(){\r
126         Object textAliaseValue;\r
127         FontRenderContext context = this.fontInfo.getFontRenderContext();\r
128         if(context.isAntiAliased()){\r
129             textAliaseValue = RenderingHints.VALUE_TEXT_ANTIALIAS_ON;\r
130         }else{\r
131             textAliaseValue = RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;\r
132         }\r
133         this.hints.put(RenderingHints.KEY_TEXT_ANTIALIASING,\r
134                        textAliaseValue);\r
135 \r
136         Object textFractionalValue;\r
137         if(context.usesFractionalMetrics()){\r
138             textFractionalValue = RenderingHints.VALUE_FRACTIONALMETRICS_ON;\r
139         }else{\r
140             textFractionalValue = RenderingHints.VALUE_FRACTIONALMETRICS_OFF;\r
141         }\r
142         this.hints.put(RenderingHints.KEY_FRACTIONALMETRICS,\r
143                        textFractionalValue);\r
144 \r
145         return;\r
146     }\r
147 \r
148     /**\r
149      * 配色を設定する。\r
150      */\r
151     private void setColorDesign(){\r
152         Color fgColor;\r
153         if(this.dialogPref.isSimpleMode()){\r
154             fgColor = COLOR_SIMPLEBG;\r
155         }else{\r
156             fgColor = COLOR_NORMALBG;\r
157         }\r
158 \r
159         setForeground(fgColor);\r
160         repaint();\r
161 \r
162         return;\r
163     }\r
164 \r
165     /**\r
166      * フォント描画設定を変更する。\r
167      * @param newFontInfo フォント設定\r
168      */\r
169     public void setFontInfo(FontInfo newFontInfo){\r
170         this.fontInfo = newFontInfo;\r
171 \r
172         updateRenderingHints();\r
173 \r
174         for(TextRow row : this.rowList){\r
175             row.setFontInfo(this.fontInfo);\r
176         }\r
177 \r
178         setColorDesign();\r
179         layoutRows();\r
180 \r
181         revalidate();\r
182         repaint();\r
183 \r
184         return;\r
185     }\r
186 \r
187     /**\r
188      * 発言表示設定を変更する。\r
189      * @param newPref 発言表示設定\r
190      */\r
191     public void setDialogPref(DialogPref newPref){\r
192         this.dialogPref = newPref;\r
193 \r
194         for(TextRow row : this.rowList){\r
195             if(row instanceof TalkDraw){\r
196                 TalkDraw talkDraw = (TalkDraw) row;\r
197                 talkDraw.setDialogPref(this.dialogPref);\r
198             }else if(row instanceof SysEventDraw){\r
199                 SysEventDraw sysDraw = (SysEventDraw) row;\r
200                 sysDraw.setDialogPref(this.dialogPref);\r
201             }\r
202         }\r
203 \r
204         setColorDesign();\r
205         layoutRows();\r
206 \r
207         revalidate();\r
208         repaint();\r
209 \r
210         return;\r
211     }\r
212 \r
213     /**\r
214      * 現在のPeriodを返す。\r
215      * @return 現在のPeriod\r
216      */\r
217     public Period getPeriod(){\r
218         return this.period;\r
219     }\r
220 \r
221     /**\r
222      * Periodを更新する。\r
223      * 新しいPeriodの表示内容はまだ反映されない。\r
224      * @param period 新しいPeriod\r
225      */\r
226     public final void setPeriod(Period period){\r
227         if(period == null){\r
228             this.period = null;\r
229             this.rowList.clear();\r
230             this.talkDrawList.clear();\r
231             return;\r
232         }\r
233 \r
234         if(   this.period == period\r
235            && period.getTopics() == this.rowList.size() ){\r
236             filterTopics();\r
237             return;\r
238         }\r
239 \r
240         this.period = period;\r
241 \r
242         this.filterContext = null;\r
243 \r
244         this.rowList.clear();\r
245         this.talkDrawList.clear();\r
246         for(Topic topic : this.period.getTopicList()){\r
247             TextRow row;\r
248             if(topic instanceof Talk){\r
249                 Talk talk = (Talk) topic;\r
250                 TalkDraw talkDraw = new TalkDraw(talk,\r
251                                                  this.dialogPref,\r
252                                                  this.fontInfo );\r
253                 this.talkDrawList.add(talkDraw);\r
254                 row = talkDraw;\r
255             }else if(topic instanceof SysEvent){\r
256                 SysEvent sysEvent = (SysEvent) topic;\r
257                 row = new SysEventDraw(sysEvent,\r
258                                        this.dialogPref,\r
259                                        this.fontInfo );\r
260             }else{\r
261                 assert false;\r
262                 continue;\r
263             }\r
264             this.rowList.add(row);\r
265         }\r
266 \r
267         filterTopics();\r
268 \r
269         clearSizeCache();\r
270 \r
271         layoutRows();\r
272 \r
273         return;\r
274     }\r
275 \r
276     /**\r
277      * 発言フィルタを設定する。\r
278      * @param filter 発言フィルタ\r
279      */\r
280     public void setTopicFilter(TopicFilter filter){\r
281         this.topicFilter = filter;\r
282         filtering();\r
283         return;\r
284     }\r
285 \r
286     /**\r
287      * 発言フィルタを適用する。\r
288      */\r
289     public void filtering(){\r
290         if(   this.topicFilter != null\r
291            && this.topicFilter.isSame(this.filterContext)){\r
292             return;\r
293         }\r
294 \r
295         if(this.topicFilter != null){\r
296             this.filterContext = this.topicFilter.getFilterContext();\r
297         }else{\r
298             this.filterContext = null;\r
299         }\r
300 \r
301         filterTopics();\r
302         layoutVertical();\r
303 \r
304         clearSelect();\r
305 \r
306         return;\r
307     }\r
308 \r
309     /**\r
310      * 検索パターンを取得する。\r
311      * @return 検索パターン\r
312      */\r
313     public RegexPattern getRegexPattern(){\r
314         return this.regexPattern;\r
315     }\r
316 \r
317     /**\r
318      * 与えられた正規表現にマッチする文字列をハイライト描画する。\r
319      * @param newPattern 検索パターン\r
320      * @return ヒット件数\r
321      */\r
322     public int setRegexPattern(RegexPattern newPattern){\r
323         this.regexPattern = newPattern;\r
324 \r
325         int total = 0;\r
326 \r
327         clearHotTarget();\r
328 \r
329         Pattern pattern = null;\r
330         if(this.regexPattern != null){\r
331             pattern = this.regexPattern.getPattern();\r
332         }\r
333 \r
334         for(TalkDraw talkDraw : this.talkDrawList){\r
335             total += talkDraw.setRegex(pattern);\r
336         }\r
337 \r
338         repaint();\r
339 \r
340         return total;\r
341     }\r
342 \r
343     /**\r
344      * 検索結果の次候補をハイライト表示する。\r
345      */\r
346     public void nextHotTarget(){\r
347         TalkDraw oldTalk = null;\r
348         int oldIndex = -1;\r
349         TalkDraw newTalk = null;\r
350         int newIndex = -1;\r
351         TalkDraw firstTalk = null;\r
352 \r
353         boolean findOld = true;\r
354         for(TalkDraw talkDraw : this.talkDrawList){\r
355             int matches = talkDraw.getRegexMatches();\r
356             if(firstTalk == null && matches > 0){\r
357                 firstTalk = talkDraw;\r
358             }\r
359             if(findOld){\r
360                 int index = talkDraw.getHotTargetIndex();\r
361                 if(index < 0) continue;\r
362                 oldTalk = talkDraw;\r
363                 oldIndex = index;\r
364                 scrollRectWithMargin(talkDraw.getHotTargetRectangle());\r
365                 if(oldIndex < matches - 1 && ! isFiltered(talkDraw) ){\r
366                     newTalk = talkDraw;\r
367                     newIndex = oldIndex + 1;\r
368                     break;\r
369                 }\r
370                 findOld = false;\r
371             }else{\r
372                 if(isFiltered(talkDraw)) continue;\r
373                 if(matches <= 0) continue;\r
374                 newTalk = talkDraw;\r
375                 newIndex = 0;\r
376                 break;\r
377             }\r
378         }\r
379 \r
380         Rectangle showRect = null;\r
381         if(oldTalk == null && firstTalk != null){\r
382             firstTalk.setHotTargetIndex(0);\r
383             showRect = firstTalk.getHotTargetRectangle();\r
384         }else if(   oldTalk != null\r
385                  && newTalk != null){\r
386             oldTalk.clearHotTarget();\r
387             newTalk.setHotTargetIndex(newIndex);\r
388             showRect = newTalk.getHotTargetRectangle();\r
389         }\r
390 \r
391         if(showRect != null){\r
392             scrollRectWithMargin(showRect);\r
393         }\r
394 \r
395         repaint();\r
396 \r
397         return;\r
398     }\r
399 \r
400     /**\r
401      * 検索結果の前候補をハイライト表示する。\r
402      */\r
403     public void prevHotTarget(){\r
404         TalkDraw oldTalk = null;\r
405         int oldIndex = -1;\r
406         TalkDraw newTalk = null;\r
407         int newIndex = -1;\r
408         TalkDraw firstTalk = null;\r
409 \r
410         boolean findOld = true;\r
411         int size = this.talkDrawList.size();\r
412         ListIterator<TalkDraw> iterator =\r
413                 this.talkDrawList.listIterator(size);\r
414         while(iterator.hasPrevious()){\r
415             TalkDraw talkDraw = iterator.previous();\r
416             int matches = talkDraw.getRegexMatches();\r
417             if(firstTalk == null && matches > 0){\r
418                 firstTalk = talkDraw;\r
419             }\r
420             if(findOld){\r
421                 int index = talkDraw.getHotTargetIndex();\r
422                 if(index < 0) continue;\r
423                 oldTalk = talkDraw;\r
424                 oldIndex = index;\r
425                 scrollRectWithMargin(talkDraw.getHotTargetRectangle());\r
426                 if(oldIndex > 0 && ! isFiltered(talkDraw) ){\r
427                     newTalk = talkDraw;\r
428                     newIndex = oldIndex - 1;\r
429                     break;\r
430                 }\r
431                 findOld = false;\r
432             }else{\r
433                 if(isFiltered(talkDraw)) continue;\r
434                 if(matches <= 0) continue;\r
435                 newTalk = talkDraw;\r
436                 newIndex = matches - 1;\r
437                 break;\r
438             }\r
439         }\r
440 \r
441         Rectangle showRect = null;\r
442         if(oldTalk == null && firstTalk != null){\r
443             int matches = firstTalk.getRegexMatches();\r
444             firstTalk.setHotTargetIndex(matches - 1);\r
445             showRect = firstTalk.getHotTargetRectangle();\r
446         }else if(   oldTalk != null\r
447                  && newTalk != null){\r
448             oldTalk.clearHotTarget();\r
449             newTalk.setHotTargetIndex(newIndex);\r
450             showRect = newTalk.getHotTargetRectangle();\r
451         }\r
452 \r
453         if(showRect != null){\r
454             scrollRectWithMargin(showRect);\r
455         }\r
456 \r
457         repaint();\r
458 \r
459         return;\r
460     }\r
461 \r
462     /**\r
463      * 検索結果の特殊ハイライト表示を解除。\r
464      */\r
465     public void clearHotTarget(){\r
466         for(TalkDraw talkDraw : this.talkDrawList){\r
467             talkDraw.clearHotTarget();\r
468         }\r
469         repaint();\r
470         return;\r
471     }\r
472 \r
473     /**\r
474      * 指定した領域に若干の上下マージンを付けて\r
475      * スクロールウィンドウに表示させる。\r
476      * @param rectangle 指定領域\r
477      */\r
478     private void scrollRectWithMargin(Rectangle rectangle){\r
479         Rectangle show = new Rectangle(rectangle);\r
480         show.y      -= MARGINTOP;\r
481         show.height += MARGINTOP + MARGINBOTTOM;\r
482 \r
483         scrollRectToVisible(show);\r
484 \r
485         return;\r
486     }\r
487 \r
488     /**\r
489      * 過去に計算した寸法を破棄する。\r
490      */\r
491     private void clearSizeCache(){\r
492         this.idealSize = null;\r
493         this.lastWidth = -1;\r
494         revalidate();\r
495         return;\r
496     }\r
497 \r
498     /**\r
499      * 指定した矩形がフィルタリング対象か判定する。\r
500      * @param row 矩形\r
501      * @return フィルタリング対象ならtrue\r
502      */\r
503     private boolean isFiltered(TextRow row){\r
504         if(this.topicFilter == null) return false;\r
505 \r
506         Topic topic;\r
507         if(row instanceof TalkDraw){\r
508             topic = ((TalkDraw)row).getTalk();\r
509         }else if(row instanceof SysEventDraw){\r
510             topic = ((SysEventDraw)row).getSysEvent();\r
511         }else{\r
512             return false;\r
513         }\r
514 \r
515         return this.topicFilter.isFiltered(topic);\r
516     }\r
517 \r
518     /**\r
519      * フィルタリング指定に従いTextRowを表示するか否か設定する。\r
520      */\r
521     private void filterTopics(){\r
522         for(TextRow row : this.rowList){\r
523             if(isFiltered(row)) row.setVisible(false);\r
524             else                row.setVisible(true);\r
525         }\r
526         return;\r
527     }\r
528 \r
529     /**\r
530      * 幅を設定する。\r
531      * 全子TextRowがリサイズされる。\r
532      * @param width コンポーネント幅\r
533      */\r
534     private void setWidth(int width){\r
535         this.lastWidth = width;\r
536         Insets insets = getInsets();\r
537         int rowWidth = width - (insets.left + insets.right);\r
538         for(TextRow row : this.rowList){\r
539             row.setWidth(rowWidth);\r
540         }\r
541 \r
542         layoutVertical();\r
543 \r
544         return;\r
545     }\r
546 \r
547     /**\r
548      * 子TextRowの縦位置レイアウトを行う。\r
549      * フィルタリングが反映される。\r
550      * TextRowは必要に応じて移動させられるがリサイズされることはない。\r
551      */\r
552     private void layoutVertical(){\r
553         Rectangle unionRect = null;\r
554         Insets insets = getInsets();\r
555         int vertPos = insets.top;\r
556 \r
557         for(TextRow row : this.rowList){\r
558             if( ! row.isVisible() ) continue;\r
559 \r
560             row.setPos(insets.left, vertPos);\r
561             Rectangle rowBound = row.getBounds();\r
562             vertPos += rowBound.height;\r
563 \r
564             if(unionRect == null){\r
565                 unionRect = new Rectangle(rowBound);\r
566             }else{\r
567                 unionRect.add(rowBound);\r
568             }\r
569         }\r
570 \r
571         if(unionRect == null){\r
572             unionRect = new Rectangle(insets.left, insets.top, 0, 0);\r
573         }\r
574 \r
575         if(this.idealSize == null){\r
576             this.idealSize = new Dimension();\r
577         }\r
578 \r
579         int newWidth  = insets.left + unionRect.width  + insets.right;\r
580         int newHeight = insets.top  + unionRect.height + insets.bottom;\r
581 \r
582         this.idealSize.setSize(newWidth, newHeight);\r
583 \r
584         setPreferredSize(this.idealSize);\r
585 \r
586         revalidate();\r
587         repaint();\r
588 \r
589         return;\r
590     }\r
591 \r
592     /**\r
593      * Rowsの縦位置を再レイアウトする。\r
594      */\r
595     public void layoutRows(){\r
596         int width = getWidth();\r
597         setWidth(width);\r
598         return;\r
599     }\r
600 \r
601     /**\r
602      * {@inheritDoc}\r
603      * @param g {@inheritDoc}\r
604      */\r
605     @Override\r
606     public void paintComponent(Graphics g){\r
607         Graphics2D g2 = (Graphics2D) g;\r
608         g2.setRenderingHints(this.hints);\r
609 \r
610         Rectangle clipRect = g2.getClipBounds();\r
611         g2.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);\r
612 \r
613         for(TextRow row : this.rowList){\r
614             if( ! row.isVisible() ) continue;\r
615 \r
616             Rectangle rowRect = row.getBounds();\r
617             if( ! rowRect.intersects(clipRect) ) continue;\r
618 \r
619             row.paint(g2);\r
620         }\r
621 \r
622         return;\r
623     }\r
624 \r
625     /**\r
626      * {@inheritDoc}\r
627      * @return {@inheritDoc}\r
628      */\r
629     @Override\r
630     public Dimension getPreferredScrollableViewportSize(){\r
631         return getPreferredSize();\r
632     }\r
633 \r
634     /**\r
635      * {@inheritDoc}\r
636      * @return {@inheritDoc}\r
637      */\r
638     @Override\r
639     public boolean getScrollableTracksViewportWidth(){\r
640         return true;\r
641     }\r
642 \r
643     /**\r
644      * {@inheritDoc}\r
645      * @return {@inheritDoc}\r
646      */\r
647     @Override\r
648     public boolean getScrollableTracksViewportHeight(){\r
649         return false;\r
650     }\r
651 \r
652     /**\r
653      * {@inheritDoc}\r
654      * @param visibleRect {@inheritDoc}\r
655      * @param orientation {@inheritDoc}\r
656      * @param direction {@inheritDoc}\r
657      * @return {@inheritDoc}\r
658      */\r
659     @Override\r
660     public int getScrollableBlockIncrement(Rectangle visibleRect,\r
661                                                int orientation,\r
662                                                int direction        ){\r
663         if(orientation == SwingConstants.VERTICAL){\r
664             return visibleRect.height;\r
665         }\r
666         return 30; // TODO フォント高 × 1.5 ぐらい?\r
667     }\r
668 \r
669     /**\r
670      * {@inheritDoc}\r
671      * @param visibleRect {@inheritDoc}\r
672      * @param orientation {@inheritDoc}\r
673      * @param direction {@inheritDoc}\r
674      * @return {@inheritDoc}\r
675      */\r
676     @Override\r
677     public int getScrollableUnitIncrement(Rectangle visibleRect,\r
678                                               int orientation,\r
679                                               int direction      ){\r
680         return 30;\r
681     }\r
682 \r
683     /**\r
684      * 任意の発言の表示が占める画面領域を返す。\r
685      * 発言がフィルタリング対象の時はnullを返す。\r
686      * @param talk 発言\r
687      * @return 領域\r
688      */\r
689     public Rectangle getTalkBounds(Talk talk){\r
690         if(   this.topicFilter != null\r
691            && this.topicFilter.isFiltered(talk)) return null;\r
692 \r
693         for(TalkDraw talkDraw : this.talkDrawList){\r
694             if(talkDraw.getTalk() == talk){\r
695                 Rectangle rect = talkDraw.getBounds();\r
696                 return rect;\r
697             }\r
698         }\r
699 \r
700         return null;\r
701     }\r
702 \r
703     /**\r
704      * ドラッグ処理を行う。\r
705      * @param from ドラッグ開始位置\r
706      * @param to 現在のドラッグ位置\r
707      */\r
708     private void drag(Point from, Point to){\r
709         Rectangle dragRegion = new Rectangle();\r
710         dragRegion.setFrameFromDiagonal(from, to);\r
711 \r
712         for(TextRow row : this.rowList){\r
713             if(isFiltered(row)) continue;\r
714             if( ! row.getBounds().intersects(dragRegion) ) continue;\r
715             row.drag(from, to);\r
716         }\r
717         repaint();\r
718         return;\r
719     }\r
720 \r
721     /**\r
722      * 選択範囲の解除。\r
723      */\r
724     private void clearSelect(){\r
725         for(TextRow row : this.rowList){\r
726             row.clearSelect();\r
727         }\r
728         repaint();\r
729         return;\r
730     }\r
731 \r
732     /**\r
733      * 与えられた点座標を包含する発言を返す。\r
734      * @param pt 点座標(JComponent基準)\r
735      * @return 点座標を含む発言。含む発言がなければnullを返す。\r
736      */\r
737     // TODO 二分探索とかしたい。\r
738     private TalkDraw getHittedTalkDraw(Point pt){\r
739         for(TalkDraw talkDraw : this.talkDrawList){\r
740             if(isFiltered(talkDraw)) continue;\r
741             Rectangle bounds = talkDraw.getBounds();\r
742             if(bounds.contains(pt)) return talkDraw;\r
743         }\r
744         return null;\r
745     }\r
746 \r
747     /**\r
748      * アンカークリック動作の処理。\r
749      * @param pt クリックポイント\r
750      */\r
751     private void hitAnchor(Point pt){\r
752         TalkDraw talkDraw = getHittedTalkDraw(pt);\r
753         if(talkDraw == null) return;\r
754 \r
755         Anchor anchor = talkDraw.getAnchor(pt);\r
756         if(anchor == null) return;\r
757 \r
758         for(AnchorHitListener listener : getAnchorHitListeners()){\r
759             AnchorHitEvent event =\r
760                     new AnchorHitEvent(this, talkDraw, anchor, pt);\r
761             listener.anchorHitted(event);\r
762         }\r
763 \r
764         return;\r
765     }\r
766 \r
767     /**\r
768      * 検索マッチ文字列クリック動作の処理。\r
769      * @param pt クリックポイント\r
770      */\r
771     private void hitRegex(Point pt){\r
772         TalkDraw talkDraw = getHittedTalkDraw(pt);\r
773         if(talkDraw == null) return;\r
774 \r
775         int index = talkDraw.getRegexMatchIndex(pt);\r
776         if(index < 0) return;\r
777 \r
778         clearHotTarget();\r
779         talkDraw.setHotTargetIndex(index);\r
780 \r
781         return;\r
782     }\r
783 \r
784     /**\r
785      * {@inheritDoc}\r
786      * アンカーヒット処理を行う。\r
787      * MouseInputListenerを参照せよ。\r
788      * @param event {@inheritDoc}\r
789      */\r
790     // TODO 距離判定がシビアすぎ\r
791     @Override\r
792     public void mouseClicked(MouseEvent event){\r
793         Point pt = event.getPoint();\r
794         if(event.getButton() == MouseEvent.BUTTON1){\r
795             clearSelect();\r
796             hitAnchor(pt);\r
797             hitRegex(pt);\r
798         }\r
799         return;\r
800     }\r
801 \r
802     /**\r
803      * {@inheritDoc}\r
804      * @param event {@inheritDoc}\r
805      */\r
806     @Override\r
807     public void mouseEntered(MouseEvent event){\r
808         // TODO ここでキーボードフォーカス処理が必要?\r
809         return;\r
810     }\r
811 \r
812     /**\r
813      * {@inheritDoc}\r
814      * @param event {@inheritDoc}\r
815      */\r
816     @Override\r
817     public void mouseExited(MouseEvent event){\r
818         return;\r
819     }\r
820 \r
821     /**\r
822      * {@inheritDoc}\r
823      * ドラッグ開始処理を行う。\r
824      * @param event {@inheritDoc}\r
825      */\r
826     @Override\r
827     public void mousePressed(MouseEvent event){\r
828         requestFocusInWindow();\r
829 \r
830         if(event.getButton() == MouseEvent.BUTTON1){\r
831             clearSelect();\r
832             this.dragFrom = event.getPoint();\r
833         }\r
834 \r
835         return;\r
836     }\r
837 \r
838     /**\r
839      * {@inheritDoc}\r
840      * ドラッグ終了処理を行う。\r
841      * @param event {@inheritDoc}\r
842      */\r
843     @Override\r
844     public void mouseReleased(MouseEvent event){\r
845         if(event.getButton() == MouseEvent.BUTTON1){\r
846             this.dragFrom = null;\r
847         }\r
848         return;\r
849     }\r
850 \r
851     /**\r
852      * {@inheritDoc}\r
853      * ドラッグ処理を行う。\r
854      * @param event {@inheritDoc}\r
855      */\r
856     // TODO ドラッグ範囲がビューポートを超えたら自動的にスクロールしてほしい。\r
857     @Override\r
858     public void mouseDragged(MouseEvent event){\r
859         if(this.dragFrom == null) return;\r
860         Point dragTo = event.getPoint();\r
861         drag(this.dragFrom, dragTo);\r
862         return;\r
863     }\r
864 \r
865     /**\r
866      * {@inheritDoc}\r
867      * @param event {@inheritDoc}\r
868      */\r
869     @Override\r
870     public void mouseMoved(MouseEvent event){\r
871         return;\r
872     }\r
873 \r
874     /**\r
875      * {@inheritDoc}\r
876      * @param event {@inheritDoc}\r
877      */\r
878     @Override\r
879     public void componentShown(ComponentEvent event){\r
880         return;\r
881     }\r
882 \r
883     /**\r
884      * {@inheritDoc}\r
885      * @param event {@inheritDoc}\r
886      */\r
887     @Override\r
888     public void componentHidden(ComponentEvent event){\r
889         return;\r
890     }\r
891 \r
892     /**\r
893      * {@inheritDoc}\r
894      * @param event {@inheritDoc}\r
895      */\r
896     @Override\r
897     public void componentMoved(ComponentEvent event){\r
898         return;\r
899     }\r
900 \r
901     /**\r
902      * {@inheritDoc}\r
903      * @param event {@inheritDoc}\r
904      */\r
905     @Override\r
906     public void componentResized(ComponentEvent event){\r
907         int width  = getWidth();\r
908         int height = getHeight();\r
909         if(width != this.lastWidth){\r
910             setWidth(width);\r
911         }\r
912         if(   this.idealSize.width != width\r
913            || this.idealSize.height != height ){\r
914             revalidate();\r
915         }\r
916         return;\r
917     }\r
918 \r
919     /**\r
920      * 選択文字列を返す。\r
921      * @return 選択文字列\r
922      */\r
923     public CharSequence getSelected(){\r
924         StringBuilder selected = new StringBuilder();\r
925 \r
926         for(TextRow row : this.rowList){\r
927             if(isFiltered(row)) continue;\r
928             try{\r
929                 row.appendSelected(selected);\r
930             }catch(IOException e){\r
931                 assert false; // ありえない\r
932                 return null;\r
933             }\r
934         }\r
935 \r
936         if(selected.length() <= 0) return null;\r
937 \r
938         return selected;\r
939     }\r
940 \r
941     /**\r
942      * 選択文字列をクリップボードにコピーする。\r
943      * @return 選択文字列\r
944      */\r
945     public CharSequence copySelected(){\r
946         CharSequence selected = getSelected();\r
947         if(selected == null) return null;\r
948         ClipboardAction.copyToClipboard(selected);\r
949         return selected;\r
950     }\r
951 \r
952     /**\r
953      * 矩形の示す一発言をクリップボードにコピーする。\r
954      * @return コピーした文字列\r
955      */\r
956     public CharSequence copyTalk(){\r
957         TalkDraw talkDraw = this.popup.lastPopupedTalkDraw;\r
958         if(talkDraw == null) return null;\r
959         Talk talk = talkDraw.getTalk();\r
960 \r
961         StringBuilder selected = new StringBuilder();\r
962 \r
963         Avatar avatar = talk.getAvatar();\r
964         selected.append(avatar.getName()).append(' ');\r
965 \r
966         String anchor = talk.getAnchorNotation();\r
967         selected.append(anchor);\r
968         if(talk.hasTalkNo()){\r
969             selected.append(' ').append(talk.getAnchorNotation_G());\r
970         }\r
971         selected.append('\n');\r
972 \r
973         selected.append(talk.getDialog());\r
974         if(selected.charAt(selected.length() - 1) != '\n'){\r
975             selected.append('\n');\r
976         }\r
977 \r
978         ClipboardAction.copyToClipboard(selected);\r
979 \r
980         return selected;\r
981     }\r
982 \r
983     /**\r
984      * ポップアップメニュートリガ座標に発言があればそれを返す。\r
985      * @return 発言\r
986      */\r
987     public Talk getPopupedTalk(){\r
988         TalkDraw talkDraw = this.popup.lastPopupedTalkDraw;\r
989         if(talkDraw == null) return null;\r
990         Talk talk = talkDraw.getTalk();\r
991         return talk;\r
992     }\r
993 \r
994     /**\r
995      * ポップアップメニュートリガ座標にアンカーがあればそれを返す。\r
996      * @return アンカー\r
997      */\r
998     public Anchor getPopupedAnchor(){\r
999         return this.popup.lastPopupedAnchor;\r
1000     }\r
1001 \r
1002     /**\r
1003      * {@inheritDoc}\r
1004      */\r
1005     @Override\r
1006     public void updateUI(){\r
1007         super.updateUI();\r
1008         this.popup.updateUI();\r
1009 \r
1010         updateInputMap();\r
1011 \r
1012         return;\r
1013     }\r
1014 \r
1015     /**\r
1016      * COPY処理を行うキーの設定をJTextFieldから流用する。\r
1017      * おそらくはCtrl-C。MacならCommand-Cかも。\r
1018      */\r
1019     private void updateInputMap(){\r
1020         InputMap thisInputMap = getInputMap();\r
1021 \r
1022         InputMap sampleInputMap;\r
1023         sampleInputMap = new JTextField().getInputMap();\r
1024         KeyStroke[] strokes = sampleInputMap.allKeys();\r
1025         for(KeyStroke stroke : strokes){\r
1026             Object bind = sampleInputMap.get(stroke);\r
1027             if(bind.equals(DefaultEditorKit.copyAction)){\r
1028                 thisInputMap.put(stroke, DefaultEditorKit.copyAction);\r
1029             }\r
1030         }\r
1031 \r
1032         return;\r
1033     }\r
1034 \r
1035     /**\r
1036      * ActionListenerを追加する。\r
1037      * @param listener リスナー\r
1038      */\r
1039     public void addActionListener(ActionListener listener){\r
1040         this.thisListenerList.add(ActionListener.class, listener);\r
1041 \r
1042         this.popup.menuCopy       .addActionListener(listener);\r
1043         this.popup.menuSelTalk    .addActionListener(listener);\r
1044         this.popup.menuJumpAnchor .addActionListener(listener);\r
1045         this.popup.menuWebTalk    .addActionListener(listener);\r
1046         this.popup.menuSummary    .addActionListener(listener);\r
1047 \r
1048         return;\r
1049     }\r
1050 \r
1051     /**\r
1052      * ActionListenerを削除する。\r
1053      * @param listener リスナー\r
1054      */\r
1055     public void removeActionListener(ActionListener listener){\r
1056         this.thisListenerList.remove(ActionListener.class, listener);\r
1057 \r
1058         this.popup.menuCopy       .removeActionListener(listener);\r
1059         this.popup.menuSelTalk    .removeActionListener(listener);\r
1060         this.popup.menuJumpAnchor .removeActionListener(listener);\r
1061         this.popup.menuWebTalk    .removeActionListener(listener);\r
1062         this.popup.menuSummary    .removeActionListener(listener);\r
1063 \r
1064         return;\r
1065     }\r
1066 \r
1067     /**\r
1068      * ActionListenerを列挙する。\r
1069      * @return すべてのActionListener\r
1070      */\r
1071     public ActionListener[] getActionListeners(){\r
1072         return this.thisListenerList.getListeners(ActionListener.class);\r
1073     }\r
1074 \r
1075     /**\r
1076      * AnchorHitListenerを追加する。\r
1077      * @param listener リスナー\r
1078      */\r
1079     public void addAnchorHitListener(AnchorHitListener listener){\r
1080         this.thisListenerList.add(AnchorHitListener.class, listener);\r
1081         return;\r
1082     }\r
1083 \r
1084     /**\r
1085      * AnchorHitListenerを削除する。\r
1086      * @param listener リスナー\r
1087      */\r
1088     public void removeAnchorHitListener(AnchorHitListener listener){\r
1089         this.thisListenerList.remove(AnchorHitListener.class, listener);\r
1090         return;\r
1091     }\r
1092 \r
1093     /**\r
1094      * AnchorHitListenerを列挙する。\r
1095      * @return すべてのAnchorHitListener\r
1096      */\r
1097     public AnchorHitListener[] getAnchorHitListeners(){\r
1098         return this.thisListenerList.getListeners(AnchorHitListener.class);\r
1099     }\r
1100 \r
1101     /**\r
1102      * {@inheritDoc}\r
1103      * @param <T> {@inheritDoc}\r
1104      * @param listenerType {@inheritDoc}\r
1105      * @return {@inheritDoc}\r
1106      */\r
1107     @Override\r
1108     public <T extends EventListener> T[] getListeners(Class<T> listenerType){\r
1109         T[] result;\r
1110         result = this.thisListenerList.getListeners(listenerType);\r
1111 \r
1112         if(result.length <= 0){\r
1113             result = super.getListeners(listenerType);\r
1114         }\r
1115 \r
1116         return result;\r
1117     }\r
1118 \r
1119     /**\r
1120      * キーボード入力用ダミーAction。\r
1121      */\r
1122     private class ProxyAction extends AbstractAction{\r
1123 \r
1124         private final String command;\r
1125 \r
1126         /**\r
1127          * コンストラクタ。\r
1128          * @param command コマンド\r
1129          * @throws NullPointerException 引数がnull\r
1130          */\r
1131         public ProxyAction(String command) throws NullPointerException{\r
1132             super();\r
1133             if(command == null) throw new NullPointerException();\r
1134             this.command = command;\r
1135             return;\r
1136         }\r
1137 \r
1138         /**\r
1139          * {@inheritDoc}\r
1140          * @param event {@inheritDoc}\r
1141          */\r
1142         @Override\r
1143         public void actionPerformed(ActionEvent event){\r
1144             Object source  = event.getSource();\r
1145             int id         = event.getID();\r
1146             String actcmd  = this.command;\r
1147             long when      = event.getWhen();\r
1148             int modifiers  = event.getModifiers();\r
1149 \r
1150             for(ActionListener listener : getActionListeners()){\r
1151                 ActionEvent newEvent = new ActionEvent(source,\r
1152                                                        id,\r
1153                                                        actcmd,\r
1154                                                        when,\r
1155                                                        modifiers );\r
1156                 listener.actionPerformed(newEvent);\r
1157             }\r
1158 \r
1159             return;\r
1160         }\r
1161     };\r
1162 \r
1163     /**\r
1164      * ポップアップメニュー。\r
1165      */\r
1166     private class DiscussionPopup extends JPopupMenu{\r
1167 \r
1168         private final JMenuItem menuCopy =\r
1169                 new JMenuItem("選択範囲をコピー");\r
1170         private final JMenuItem menuSelTalk =\r
1171                 new JMenuItem("この発言をコピー");\r
1172         private final JMenuItem menuJumpAnchor =\r
1173                 new JMenuItem("アンカーの示す先へジャンプ");\r
1174         private final JMenuItem menuWebTalk =\r
1175                 new JMenuItem("この発言をブラウザで表示...");\r
1176         private final JMenuItem menuSummary =\r
1177                 new JMenuItem("発言を集計...");\r
1178 \r
1179         private TalkDraw lastPopupedTalkDraw;\r
1180         private Anchor lastPopupedAnchor;\r
1181 \r
1182         /**\r
1183          * コンストラクタ。\r
1184          */\r
1185         public DiscussionPopup(){\r
1186             super();\r
1187 \r
1188             add(this.menuCopy);\r
1189             add(this.menuSelTalk);\r
1190             addSeparator();\r
1191             add(this.menuJumpAnchor);\r
1192             add(this.menuWebTalk);\r
1193             addSeparator();\r
1194             add(this.menuSummary);\r
1195 \r
1196             this.menuCopy\r
1197                 .setActionCommand(ActionManager.CMD_COPY);\r
1198             this.menuSelTalk\r
1199                 .setActionCommand(ActionManager.CMD_COPYTALK);\r
1200             this.menuJumpAnchor\r
1201                 .setActionCommand(ActionManager.CMD_JUMPANCHOR);\r
1202             this.menuWebTalk\r
1203                 .setActionCommand(ActionManager.CMD_WEBTALK);\r
1204             this.menuSummary\r
1205                 .setActionCommand(ActionManager.CMD_DAYSUMMARY);\r
1206 \r
1207             this.menuWebTalk.setIcon(GUIUtils.getWWWIcon());\r
1208 \r
1209             return;\r
1210         }\r
1211 \r
1212         /**\r
1213          * {@inheritDoc}\r
1214          * @param comp {@inheritDoc}\r
1215          * @param x {@inheritDoc}\r
1216          * @param y {@inheritDoc}\r
1217          */\r
1218         @Override\r
1219         public void show(Component comp, int x, int y){\r
1220             Point point = new Point(x, y);\r
1221 \r
1222             this.lastPopupedTalkDraw = getHittedTalkDraw(point);\r
1223             if(this.lastPopupedTalkDraw != null){\r
1224                 this.menuSelTalk.setEnabled(true);\r
1225                 this.menuWebTalk.setEnabled(true);\r
1226             }else{\r
1227                 this.menuSelTalk.setEnabled(false);\r
1228                 this.menuWebTalk.setEnabled(false);\r
1229             }\r
1230 \r
1231             if(this.lastPopupedTalkDraw != null){\r
1232                 this.lastPopupedAnchor =\r
1233                         this.lastPopupedTalkDraw.getAnchor(point);\r
1234             }else{\r
1235                 this.lastPopupedAnchor = null;\r
1236             }\r
1237 \r
1238             if(this.lastPopupedAnchor != null){\r
1239                 this.menuJumpAnchor.setEnabled(true);\r
1240             }else{\r
1241                 this.menuJumpAnchor.setEnabled(false);\r
1242             }\r
1243 \r
1244             if(getSelected() != null){\r
1245                 this.menuCopy.setEnabled(true);\r
1246             }else{\r
1247                 this.menuCopy.setEnabled(false);\r
1248             }\r
1249 \r
1250             super.show(comp, x, y);\r
1251 \r
1252             return;\r
1253         }\r
1254     }\r
1255 \r
1256     // TODO シンプルモードの追加\r
1257     // Period変更を追跡するリスナ化\r
1258 }\r