OSDN Git Service

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