OSDN Git Service

mainエントリのパッケージを変更。
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / glyph / TalkDraw.java
1 /*
2  * 会話部描画
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.Font;
12 import java.awt.Graphics2D;
13 import java.awt.Point;
14 import java.awt.Polygon;
15 import java.awt.Rectangle;
16 import java.awt.RenderingHints;
17 import java.awt.image.BufferedImage;
18 import java.io.IOException;
19 import java.text.DateFormat;
20 import java.util.LinkedList;
21 import java.util.List;
22 import java.util.regex.Pattern;
23 import jp.sfjp.jindolf.data.Anchor;
24 import jp.sfjp.jindolf.data.Avatar;
25 import jp.sfjp.jindolf.data.DialogPref;
26 import jp.sfjp.jindolf.data.Period;
27 import jp.sfjp.jindolf.data.Talk;
28 import jp.sfjp.jindolf.data.Village;
29 import jp.sfjp.jindolf.util.GUIUtils;
30 import jp.sourceforge.jindolf.corelib.TalkType;
31
32 /**
33  * 会話部の描画。
34  * 会話部描画領域は、キャプション部と発言部から構成される。
35  */
36 public class TalkDraw extends AbstractTextRow{
37
38     /** 通常会話の色。 */
39     public static final Color COLOR_PUBLIC   = new Color(0xffffff);
40     /** 狼間ささやきの色。 */
41     public static final Color COLOR_WOLFONLY = new Color(0xff7777);
42     /** 灰色発言の色。 */
43     public static final Color COLOR_PRIVATE  = new Color(0x939393);
44     /** 墓下発言の色。 */
45     public static final Color COLOR_GRAVE    = new Color(0x9fb7cf);
46
47     private static final Color COLOR_CAPTIONFG = Color.WHITE;
48     private static final Color COLOR_DIALOGFG  = Color.BLACK;
49
50     private static final Color COLOR_SIMPLEFG = Color.BLACK;
51     private static final Color COLOR_SIMPLEBG = Color.WHITE;
52
53     private static final int BALOONTIP_WIDTH = 16;
54     private static final int BALOONTIP_HEIGHT = 8;
55     private static final int UPPER_MARGIN = 5;
56     private static final int UNDER_MARGIN = 10;
57     private static final int OFFSET_ANCHOR = 36;
58     private static final int CAPTION_DIALOG_GAP = 3;
59
60     private static final Color COLOR_TRANS = new Color(0, 0, 0, 0);
61     private static final int BALOON_R = 10;
62     private static final BufferedImage BALOON_PUBLIC;
63     private static final BufferedImage BALOON_WOLFONLY;
64     private static final BufferedImage BALOON_GRAVE;
65     private static final BufferedImage BALOON_PRIVATE;
66     private static final BufferedImage SQUARE_PUBLIC;
67     private static final BufferedImage SQUARE_WOLFONLY;
68     private static final BufferedImage SQUARE_GRAVE;
69     private static final BufferedImage SQUARE_PRIVATE;
70
71     private static final float ANCHOR_FONT_RATIO = 0.9f;
72
73     static{
74         BALOON_PUBLIC   = createWedgeImage(COLOR_PUBLIC);
75         BALOON_WOLFONLY = createBubbleImage(COLOR_WOLFONLY);
76         BALOON_PRIVATE  = createBubbleImage(COLOR_PRIVATE);
77         BALOON_GRAVE    = createBubbleImage(COLOR_GRAVE);
78         SQUARE_PUBLIC   = createSquareImage(COLOR_PUBLIC);
79         SQUARE_WOLFONLY = createSquareImage(COLOR_WOLFONLY);
80         SQUARE_GRAVE    = createSquareImage(COLOR_GRAVE);
81         SQUARE_PRIVATE  = createSquareImage(COLOR_PRIVATE);
82     }
83
84
85     private final Talk talk;
86     private Anchor showingAnchor;
87
88     private final GlyphDraw caption;
89     private BufferedImage faceImage;
90     private final GlyphDraw dialog;
91     private final List<AnchorDraw> anchorTalks = new LinkedList<AnchorDraw>();
92     private Point imageOrigin;
93     private Point dialogOrigin;
94     private Point tipOrigin;
95     private int baloonWidth;
96     private int baloonHeight;
97
98     private FontInfo anchorFontInfo;
99     private DialogPref dialogPref;
100
101
102     /**
103      * コンストラクタ。
104      * @param talk 一発言
105      */
106     public TalkDraw(Talk talk){
107         this(talk, new DialogPref(), FontInfo.DEFAULT_FONTINFO);
108         return;
109     }
110
111     /**
112      * コンストラクタ。
113      * @param talk 一発言
114      * @param dialogPref 発言表示設定
115      * @param fontInfo フォント設定
116      */
117     public TalkDraw(Talk talk, DialogPref dialogPref, FontInfo fontInfo){
118         super(fontInfo);
119
120         this.talk = talk;
121         this.anchorFontInfo = deriveAnchorFontInfo(this.fontInfo);
122         this.dialogPref = dialogPref;
123
124         this.faceImage = getFaceImage();
125         this.caption = new GlyphDraw(getCaptionString(), this.fontInfo);
126         this.dialog  = new GlyphDraw(this.talk.getDialog(), this.fontInfo);
127
128         setColorDesign();
129
130         Period period = this.talk.getPeriod();
131         List<Anchor> anchorList = Anchor.getAnchorList(this.talk.getDialog(),
132                                                        period.getDay() );
133         this.dialog.setAnchorSet(anchorList);
134
135         return;
136     }
137
138
139     /**
140      * 指定した色で描画したクサビイメージを取得する。
141      * @param color 色
142      * @return クサビイメージ
143      */
144     private static BufferedImage createWedgeImage(Color color){
145         BufferedImage image;
146         image = new BufferedImage(BALOONTIP_WIDTH,
147                                   BALOONTIP_HEIGHT,
148                                   BufferedImage.TYPE_INT_ARGB);
149         Graphics2D g2 = image.createGraphics();
150         RenderingHints renderHints = GUIUtils.getQualityHints();
151         g2.addRenderingHints(renderHints);
152         g2.setColor(COLOR_TRANS);
153         g2.fillRect(0, 0, BALOONTIP_WIDTH, BALOONTIP_HEIGHT);
154         g2.setColor(color);
155         Polygon poly = new Polygon();
156         poly.addPoint(8, 8);
157         poly.addPoint(16, 8);
158         poly.addPoint(16, 0);
159         g2.fillPolygon(poly);
160         return image;
161     }
162
163     /**
164      * 指定した色で描画した泡イメージを取得する。
165      * @param color 色
166      * @return 泡イメージ
167      */
168     private static BufferedImage createBubbleImage(Color color){
169         BufferedImage image;
170         image = new BufferedImage(BALOONTIP_WIDTH,
171                                   BALOONTIP_HEIGHT,
172                                   BufferedImage.TYPE_INT_ARGB);
173         Graphics2D g2 = image.createGraphics();
174         RenderingHints renderHints = GUIUtils.getQualityHints();
175         g2.addRenderingHints(renderHints);
176         g2.setColor(COLOR_TRANS);
177         g2.fillRect(0, 0, BALOONTIP_WIDTH, BALOONTIP_HEIGHT);
178         g2.setColor(color);
179         g2.fillOval(2, 4, 4, 4);
180         g2.fillOval(8, 2, 6, 6);
181         return image;
182     }
183
184     /**
185      * 指定した色で描画した長方形イメージを返す。
186      * @param color 色
187      * @return 長方形イメージ
188      */
189     private static BufferedImage createSquareImage(Color color){
190         BufferedImage image;
191         image = new BufferedImage(BALOONTIP_WIDTH,
192                                   BALOONTIP_HEIGHT,
193                                   BufferedImage.TYPE_INT_ARGB);
194         Graphics2D g2 = image.createGraphics();
195         RenderingHints renderHints = GUIUtils.getQualityHints();
196         g2.addRenderingHints(renderHints);
197         g2.setColor(color);
198         g2.fillRect(0, 0, BALOONTIP_WIDTH, BALOONTIP_HEIGHT);
199         return image;
200     }
201
202     /**
203      * 会話表示用フォントからアンカー表示用フォントを派生させる。
204      * @param font 派生元フォント
205      * @return 派生先フォント
206      */
207     private static Font deriveAnchorFont(Font font){
208         float fontSize = font.getSize2D();
209         float newSize = fontSize * ANCHOR_FONT_RATIO;
210         return font.deriveFont(newSize);
211     }
212
213     /**
214      * 会話表示用フォント設定からアンカー表示用フォント設定を派生させる。
215      * @param info 派生元フォント設定
216      * @return 派生先フォント設定
217      */
218     private static FontInfo deriveAnchorFontInfo(FontInfo info){
219         Font newFont = deriveAnchorFont(info.getFont());
220         FontInfo result = info.deriveFont(newFont);
221         return result;
222     }
223
224     /**
225      * 発言種別毎の色を返す。
226      * @param type 発言種別
227      * @return 色
228      */
229     public static Color getTypedColor(TalkType type){
230         Color result;
231
232         switch(type){
233         case PUBLIC:   result = TalkDraw.COLOR_PUBLIC;   break;
234         case WOLFONLY: result = TalkDraw.COLOR_WOLFONLY; break;
235         case GRAVE:    result = TalkDraw.COLOR_GRAVE;    break;
236         case PRIVATE:  result = TalkDraw.COLOR_PRIVATE;  break;
237         default:       return null;
238         }
239
240         return result;
241     }
242
243     /**
244      * 配色を設定する。
245      */
246     private void setColorDesign(){
247         if(this.dialogPref.isSimpleMode()){
248             this.caption.setColor(COLOR_SIMPLEFG);
249         }else{
250             this.caption.setColor(COLOR_CAPTIONFG);
251         }
252
253         this.dialog.setColor(COLOR_DIALOGFG);
254
255         return;
256     }
257
258     /**
259      * Talk取得。
260      * @return Talkインスタンス
261      */
262     public Talk getTalk(){
263         return this.talk;
264     }
265
266     /**
267      * 顔イメージを返す。
268      * @return 顔イメージ
269      */
270     private BufferedImage getFaceImage(){
271         Village village = this.talk.getPeriod().getVillage();
272         Avatar avatar = this.talk.getAvatar();
273
274         boolean useBodyImage = this.dialogPref.useBodyImage();
275         boolean useMonoImage = this.dialogPref.useMonoImage();
276
277         BufferedImage image;
278         if(this.talk.isGrave()){
279             if(useMonoImage){
280                 if(useBodyImage){
281                     image = village.getAvatarBodyMonoImage(avatar);
282                 }else{
283                     image = village.getAvatarFaceMonoImage(avatar);
284                 }
285             }else{
286                 if(useBodyImage){
287                     image = village.getGraveBodyImage();
288                 }else{
289                     image = village.getGraveImage();
290                 }
291             }
292         }else{
293             if(useBodyImage){
294                 image = village.getAvatarBodyImage(avatar);
295             }else{
296                 image = village.getAvatarFaceImage(avatar);
297             }
298         }
299
300         return image;
301     }
302
303     /**
304      * キャプション文字列を取得する。
305      * @return キャプション文字列
306      */
307     private CharSequence getCaptionString(){
308         StringBuilder result = new StringBuilder();
309
310         Avatar avatar = this.talk.getAvatar();
311
312         if(this.talk.hasTalkNo()){
313             result.append(this.talk.getAnchorNotation_G()).append(' ');
314         }
315         result.append(avatar.getFullName()).append(' ');
316         result.append(this.talk.getAnchorNotation());
317         result.append('\n');
318
319         DateFormat dform =
320             DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
321                                            DateFormat.MEDIUM);
322         long epoch = this.talk.getTimeFromID();
323         String decoded = dform.format(epoch);
324         result.append(decoded);
325
326         int count = this.talk.getTalkCount();
327         if(count > 0){
328             TalkType type = this.talk.getTalkType();
329             result.append(" (").append(Talk.encodeColorName(type));
330             result.append('#').append(count).append(')');
331         }
332
333         int charNum = this.talk.getTotalChars();
334         if(charNum > 0){
335             result.append(' ').append(charNum).append('字');
336         }
337
338         return result;
339     }
340
341     /**
342      * 会話部背景色を返す。
343      * @return 会話部背景色
344      */
345     protected Color getTalkBgColor(){
346         if(this.dialogPref.isSimpleMode()) return COLOR_SIMPLEBG;
347
348         TalkType type = this.talk.getTalkType();
349         Color result = getTypedColor(type);
350
351         return result;
352     }
353
354     /**
355      * {@inheritDoc}
356      * @return {@inheritDoc}
357      */
358     @Override
359     public Rectangle recalcBounds(){
360         int newWidth = getWidth();
361
362         int imageWidth  = 0;
363         int imageHeight = 0;
364         if( ! this.dialogPref.isSimpleMode()){
365             imageWidth  = this.faceImage.getWidth(null);
366             imageHeight = this.faceImage.getHeight(null);
367         }
368
369         int tipWidth = BALOON_WOLFONLY.getWidth();
370
371         int modWidth;
372         int minWidth = imageWidth + tipWidth + BALOON_R * 2;
373         if(newWidth < minWidth) modWidth = minWidth;
374         else                    modWidth = newWidth;
375
376         this.caption.setWidth(modWidth);
377         int captionWidth  = this.caption.getWidth();
378         int captionHeight = this.caption.getHeight() + CAPTION_DIALOG_GAP;
379
380         this.dialog.setWidth(modWidth - minWidth);
381         int dialogWidth  = this.dialog.getWidth();
382         int dialogHeight = this.dialog.getHeight();
383
384         if(this.dialogPref.alignBaloonWidth()){
385             this.baloonWidth  = (modWidth - minWidth) + BALOON_R * 2;
386         }else{
387             this.baloonWidth  = dialogWidth + BALOON_R * 2;
388         }
389         this.baloonHeight = dialogHeight + BALOON_R * 2;
390
391         int imageAndDialogWidth = imageWidth + tipWidth + this.baloonWidth;
392
393         int totalWidth = Math.max(captionWidth, imageAndDialogWidth);
394
395         int totalHeight = captionHeight;
396         totalHeight += Math.max(imageHeight, this.baloonHeight);
397
398         int imageYpos = captionHeight;
399         int dialogYpos = captionHeight;
400         int tipYpos = captionHeight;
401         if(imageHeight < this.baloonHeight){
402             imageYpos += (this.baloonHeight - imageHeight) / 2;
403             tipYpos += (this.baloonHeight - BALOON_WOLFONLY.getHeight()) / 2;
404             dialogYpos += BALOON_R;
405         }else{
406             dialogYpos += (imageHeight - this.baloonHeight) / 2 + BALOON_R;
407             tipYpos += (imageHeight - BALOON_WOLFONLY.getHeight()) / 2;
408         }
409
410         this.imageOrigin = new Point(0, imageYpos);
411         this.caption.setPos(this.bounds.x + 0, this.bounds.y + 0);
412         this.dialogOrigin =
413                 new Point(imageWidth+tipWidth+BALOON_R, dialogYpos);
414         this.dialog.setPos(this.bounds.x + imageWidth+tipWidth+BALOON_R,
415                            this.bounds.y + dialogYpos);
416         this.tipOrigin = new Point(imageWidth, tipYpos);
417
418         for(AnchorDraw anchorDraw : this.anchorTalks){
419             anchorDraw.setWidth(modWidth - OFFSET_ANCHOR);
420             totalHeight += anchorDraw.getHeight();
421         }
422
423         if(   this.dialogPref.isSimpleMode()
424            || this.dialogPref.alignBaloonWidth() ){
425             this.bounds.width = newWidth;
426         }else{
427             this.bounds.width = totalWidth;
428         }
429         this.bounds.height = UPPER_MARGIN + totalHeight + UNDER_MARGIN;
430
431         return this.bounds;
432     }
433
434     /**
435      * {@inheritDoc}
436      * @param xPos {@inheritDoc}
437      * @param yPos {@inheritDoc}
438      */
439     @Override
440     public void setPos(int xPos, int yPos){
441         super.setPos(xPos, yPos);
442         this.caption.setPos(this.bounds.x, this.bounds.y + UPPER_MARGIN);
443         this.dialog.setPos(this.bounds.x + this.dialogOrigin.x,
444                            this.bounds.y + this.dialogOrigin.y
445                                          + UPPER_MARGIN);
446         return;
447     }
448
449     /**
450      * アイコンイメージとフキダシを繋ぐ補助イメージを返す。
451      * @return 補助イメージ
452      */
453     private BufferedImage getTipImage(){
454         BufferedImage tip;
455
456         TalkType type = this.talk.getTalkType();
457
458         if(this.dialogPref.isSimpleMode()){
459             switch(type){
460             case PUBLIC:   tip = SQUARE_PUBLIC;   break;
461             case WOLFONLY: tip = SQUARE_WOLFONLY; break;
462             case GRAVE:    tip = SQUARE_GRAVE;    break;
463             case PRIVATE:  tip = SQUARE_PRIVATE;  break;
464             default:
465                 assert false;
466                 tip = null;
467                 break;
468             }
469         }else{
470             switch(type){
471             case PUBLIC:   tip = BALOON_PUBLIC;   break;
472             case WOLFONLY: tip = BALOON_WOLFONLY; break;
473             case GRAVE:    tip = BALOON_GRAVE;    break;
474             case PRIVATE:  tip = BALOON_PRIVATE;  break;
475             default:
476                 assert false;
477                 tip = null;
478                 break;
479             }
480         }
481
482         return tip;
483     }
484
485     /**
486      * {@inheritDoc}
487      * @param g {@inheritDoc}
488      */
489     @Override
490     public void paint(Graphics2D g){
491         final int xPos = this.bounds.x;
492         final int yPos = this.bounds.y + UPPER_MARGIN;
493
494         this.caption.paint(g);
495
496         if(this.dialogPref.isSimpleMode() ){
497             g.drawLine(xPos,                     this.bounds.y,
498                        xPos + this.bounds.width, this.bounds.y );
499         }else{
500             g.drawImage(this.faceImage,
501                         xPos + this.imageOrigin.x,
502                         yPos + this.imageOrigin.y,
503                         null );
504         }
505
506         BufferedImage tip = getTipImage();
507         g.drawImage(tip,
508                     xPos + this.tipOrigin.x,
509                     yPos + this.tipOrigin.y,
510                     null );
511
512         g.setColor(getTalkBgColor());
513         g.fillRoundRect(
514                 xPos + this.dialogOrigin.x - BALOON_R,
515                 yPos + this.dialogOrigin.y - BALOON_R,
516                 this.baloonWidth,
517                 this.baloonHeight,
518                 BALOON_R,
519                 BALOON_R );
520
521         this.dialog.paint(g);
522
523         int anchorX = xPos + OFFSET_ANCHOR;
524         int anchorY = yPos + this.dialogOrigin.y + baloonHeight;
525
526         for(AnchorDraw anchorDraw : this.anchorTalks){
527             anchorDraw.setPos(anchorX, anchorY);
528             anchorDraw.paint(g);
529             anchorY += anchorDraw.getHeight();
530         }
531
532         return;
533     }
534
535     /**
536      * {@inheritDoc}
537      * @param fontInfo {@inheritDoc}
538      */
539     @Override
540     public void setFontInfo(FontInfo fontInfo){
541         super.setFontInfo(fontInfo);
542
543         this.anchorFontInfo = deriveAnchorFontInfo(this.fontInfo);
544
545         this.caption.setFontInfo(this.fontInfo);
546         this.dialog .setFontInfo(this.fontInfo);
547
548         for(AnchorDraw anchorDraw : this.anchorTalks){
549             anchorDraw.setFontInfo(this.anchorFontInfo);
550         }
551
552         recalcBounds();
553
554         return;
555     }
556
557     /**
558      * 発言設定を更新する。
559      * @param pref 発言設定
560      */
561     public void setDialogPref(DialogPref pref){
562         this.dialogPref = pref;
563         this.faceImage = getFaceImage();
564
565         for(AnchorDraw anchorDraw : this.anchorTalks){
566             anchorDraw.setDialogPref(this.dialogPref);
567         }
568
569         setColorDesign();
570         recalcBounds();
571
572         return;
573     }
574
575     /**
576      * {@inheritDoc}
577      * @param from {@inheritDoc}
578      * @param to {@inheritDoc}
579      */
580     @Override
581     public void drag(Point from, Point to){
582         this.caption.drag(from, to);
583         this.dialog.drag(from, to);
584         for(AnchorDraw anchorDraw : this.anchorTalks){
585             anchorDraw.drag(from, to);
586         }
587         return;
588     }
589
590     /**
591      * {@inheritDoc}
592      * @param appendable {@inheritDoc}
593      * @return {@inheritDoc}
594      * @throws java.io.IOException {@inheritDoc}
595      */
596     @Override
597     public Appendable appendSelected(Appendable appendable)
598             throws IOException{
599         this.caption.appendSelected(appendable);
600         this.dialog .appendSelected(appendable);
601
602         for(AnchorDraw anchorDraw : this.anchorTalks){
603             anchorDraw.appendSelected(appendable);
604         }
605
606         return appendable;
607     }
608
609     /**
610      * {@inheritDoc}
611      */
612     @Override
613     public void clearSelect(){
614         this.caption.clearSelect();
615         this.dialog.clearSelect();
616         for(AnchorDraw anchorDraw : this.anchorTalks){
617             anchorDraw.clearSelect();
618         }
619         return;
620     }
621
622     /**
623      * 与えられた座標にアンカー文字列が存在すればAnchorを返す。
624      * @param pt 座標
625      * @return アンカー
626      */
627     public Anchor getAnchor(Point pt){
628         Anchor result = this.dialog.getAnchor(pt);
629         return result;
630     }
631
632     /**
633      * アンカーを展開表示する。
634      * アンカーにnullを指定すればアンカー表示は非表示となる。
635      * @param anchor アンカー
636      * @param talkList アンカーの示す一連のTalk
637      */
638     public void showAnchorTalks(Anchor anchor, List<Talk> talkList){
639         if(anchor == null || this.showingAnchor == anchor){
640             this.showingAnchor = null;
641             this.anchorTalks.clear();
642             recalcBounds();
643             return;
644         }
645
646         this.showingAnchor = anchor;
647
648         this.anchorTalks.clear();
649         for(Talk anchorTalk : talkList){
650             AnchorDraw anchorDraw =
651                     new AnchorDraw(anchorTalk,
652                                    this.dialogPref,
653                                    this.anchorFontInfo );
654             this.anchorTalks.add(anchorDraw);
655         }
656
657         recalcBounds();
658
659         return;
660     }
661
662     /**
663      * 与えられた座標に検索マッチ文字列があればそのインデックスを返す。
664      * @param pt 座標
665      * @return 検索マッチインデックス
666      */
667     public int getRegexMatchIndex(Point pt){
668         int index = this.dialog.getRegexMatchIndex(pt);
669         return index;
670     }
671
672     /**
673      * 検索文字列パターンを設定する。
674      * @param searchRegex パターン
675      * @return ヒット数
676      */
677     public int setRegex(Pattern searchRegex){
678         int total = 0;
679
680         total += this.dialog.setRegex(searchRegex);
681 /*
682         for(AnchorDraw anchorDraw : this.anchorTalks){
683             total += anchorDraw.setRegex(searchRegex);
684         }
685 */ // TODO よくわからんので保留
686         return total;
687     }
688
689     /**
690      * 検索ハイライトインデックスを返す。
691      * @return 検索ハイライトインデックス。見つからなければ-1。
692      */
693     public int getHotTargetIndex(){
694         return this.dialog.getHotTargetIndex();
695     }
696
697     /**
698      * 検索ハイライトを設定する。
699      * @param index ハイライトインデックス。負ならハイライト全クリア。
700      */
701     public void setHotTargetIndex(int index){
702         this.dialog.setHotTargetIndex(index);
703         return;
704     }
705
706     /**
707      * 検索一致件数を返す。
708      * @return 検索一致件数
709      */
710     public int getRegexMatches(){
711         return this.dialog.getRegexMatches();
712     }
713
714     /**
715      * 特別な検索ハイライト描画をクリアする。
716      */
717     public void clearHotTarget(){
718         this.dialog.clearHotTarget();
719         return;
720     }
721
722     /**
723      * 特別な検索ハイライト領域の寸法を返す。
724      * @return ハイライト領域寸法
725      */
726     public Rectangle getHotTargetRectangle(){
727         return this.dialog.getHotTargetRectangle();
728     }
729
730 }