4 * License : The MIT License
5 * Copyright(c) 2008 olyutorskii
8 package jp.sfjp.jindolf.glyph;
10 import java.awt.Color;
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.sfjp.jindolf.view.AvatarPics;
31 import jp.sourceforge.jindolf.corelib.TalkType;
35 * 会話部描画領域は、キャプション部と発言部から構成される。
37 public class TalkDraw extends AbstractTextRow{
40 public static final Color COLOR_PUBLIC = new Color(0xffffff);
42 public static final Color COLOR_WOLFONLY = new Color(0xff7777);
44 public static final Color COLOR_PRIVATE = new Color(0x939393);
46 public static final Color COLOR_GRAVE = new Color(0x9fb7cf);
48 private static final Color COLOR_CAPTIONFG = Color.WHITE;
49 private static final Color COLOR_DIALOGFG = Color.BLACK;
51 private static final Color COLOR_SIMPLEFG = Color.BLACK;
52 private static final Color COLOR_SIMPLEBG = Color.WHITE;
54 private static final int BALOONTIP_WIDTH = 16;
55 private static final int BALOONTIP_HEIGHT = 8;
56 private static final int UPPER_MARGIN = 5;
57 private static final int UNDER_MARGIN = 10;
58 private static final int OFFSET_ANCHOR = 36;
59 private static final int CAPTION_DIALOG_GAP = 3;
61 private static final Color COLOR_TRANS = new Color(0, 0, 0, 0);
62 private static final int BALOON_R = 10;
63 private static final BufferedImage BALOON_PUBLIC;
64 private static final BufferedImage BALOON_WOLFONLY;
65 private static final BufferedImage BALOON_GRAVE;
66 private static final BufferedImage BALOON_PRIVATE;
67 private static final BufferedImage SQUARE_PUBLIC;
68 private static final BufferedImage SQUARE_WOLFONLY;
69 private static final BufferedImage SQUARE_GRAVE;
70 private static final BufferedImage SQUARE_PRIVATE;
72 private static final float ANCHOR_FONT_RATIO = 0.9f;
75 BALOON_PUBLIC = createWedgeImage(COLOR_PUBLIC);
76 BALOON_WOLFONLY = createBubbleImage(COLOR_WOLFONLY);
77 BALOON_PRIVATE = createBubbleImage(COLOR_PRIVATE);
78 BALOON_GRAVE = createBubbleImage(COLOR_GRAVE);
79 SQUARE_PUBLIC = createSquareImage(COLOR_PUBLIC);
80 SQUARE_WOLFONLY = createSquareImage(COLOR_WOLFONLY);
81 SQUARE_GRAVE = createSquareImage(COLOR_GRAVE);
82 SQUARE_PRIVATE = createSquareImage(COLOR_PRIVATE);
86 private final Talk talk;
87 private Anchor showingAnchor;
89 private final GlyphDraw caption;
90 private BufferedImage faceImage;
91 private final GlyphDraw dialog;
92 private final List<AnchorDraw> anchorTalks = new LinkedList<>();
93 private Point imageOrigin;
94 private Point dialogOrigin;
95 private Point tipOrigin;
96 private int baloonWidth;
97 private int baloonHeight;
99 private FontInfo anchorFontInfo;
100 private DialogPref dialogPref;
107 public TalkDraw(Talk talk){
108 this(talk, new DialogPref(), FontInfo.DEFAULT_FONTINFO);
115 * @param dialogPref 発言表示設定
116 * @param fontInfo フォント設定
118 public TalkDraw(Talk talk, DialogPref dialogPref, FontInfo fontInfo){
122 this.anchorFontInfo = deriveAnchorFontInfo(this.fontInfo);
123 this.dialogPref = dialogPref;
125 this.faceImage = getFaceImage();
126 this.caption = new GlyphDraw(getCaptionString(), this.fontInfo);
127 this.dialog = new GlyphDraw(this.talk.getDialog(), this.fontInfo);
131 Period period = this.talk.getPeriod();
132 List<Anchor> anchorList = Anchor.getAnchorList(this.talk.getDialog(),
134 this.dialog.setAnchorSet(anchorList);
141 * 指定した色で描画したクサビイメージを取得する。
145 private static BufferedImage createWedgeImage(Color color){
147 image = new BufferedImage(BALOONTIP_WIDTH,
149 BufferedImage.TYPE_INT_ARGB);
150 Graphics2D g2 = image.createGraphics();
151 RenderingHints renderHints = GUIUtils.getQualityHints();
152 g2.addRenderingHints(renderHints);
153 g2.setColor(COLOR_TRANS);
154 g2.fillRect(0, 0, BALOONTIP_WIDTH, BALOONTIP_HEIGHT);
156 Polygon poly = new Polygon();
158 poly.addPoint(16, 8);
159 poly.addPoint(16, 0);
160 g2.fillPolygon(poly);
165 * 指定した色で描画した泡イメージを取得する。
169 private static BufferedImage createBubbleImage(Color color){
171 image = new BufferedImage(BALOONTIP_WIDTH,
173 BufferedImage.TYPE_INT_ARGB);
174 Graphics2D g2 = image.createGraphics();
175 RenderingHints renderHints = GUIUtils.getQualityHints();
176 g2.addRenderingHints(renderHints);
177 g2.setColor(COLOR_TRANS);
178 g2.fillRect(0, 0, BALOONTIP_WIDTH, BALOONTIP_HEIGHT);
180 g2.fillOval(2, 4, 4, 4);
181 g2.fillOval(8, 2, 6, 6);
186 * 指定した色で描画した長方形イメージを返す。
190 private static BufferedImage createSquareImage(Color color){
192 image = new BufferedImage(BALOONTIP_WIDTH,
194 BufferedImage.TYPE_INT_ARGB);
195 Graphics2D g2 = image.createGraphics();
196 RenderingHints renderHints = GUIUtils.getQualityHints();
197 g2.addRenderingHints(renderHints);
199 g2.fillRect(0, 0, BALOONTIP_WIDTH, BALOONTIP_HEIGHT);
204 * 会話表示用フォントからアンカー表示用フォントを派生させる。
205 * @param font 派生元フォント
208 private static Font deriveAnchorFont(Font font){
209 float fontSize = font.getSize2D();
210 float newSize = fontSize * ANCHOR_FONT_RATIO;
211 return font.deriveFont(newSize);
215 * 会話表示用フォント設定からアンカー表示用フォント設定を派生させる。
216 * @param info 派生元フォント設定
219 private static FontInfo deriveAnchorFontInfo(FontInfo info){
220 Font newFont = deriveAnchorFont(info.getFont());
221 FontInfo result = info.deriveFont(newFont);
230 public static Color getTypedColor(TalkType type){
234 case PUBLIC: result = TalkDraw.COLOR_PUBLIC; break;
235 case WOLFONLY: result = TalkDraw.COLOR_WOLFONLY; break;
236 case GRAVE: result = TalkDraw.COLOR_GRAVE; break;
237 case PRIVATE: result = TalkDraw.COLOR_PRIVATE; break;
238 default: return null;
247 private void setColorDesign(){
248 if(this.dialogPref.isSimpleMode()){
249 this.caption.setColor(COLOR_SIMPLEFG);
251 this.caption.setColor(COLOR_CAPTIONFG);
254 this.dialog.setColor(COLOR_DIALOGFG);
263 public Talk getTalk(){
271 private BufferedImage getFaceImage(){
272 Village village = this.talk.getPeriod().getVillage();
273 AvatarPics avatarPics = village.getAvatarPics();
274 Avatar avatar = this.talk.getAvatar();
276 boolean useBodyImage = this.dialogPref.useBodyImage();
277 boolean useMonoImage = this.dialogPref.useMonoImage();
280 if(this.talk.isGrave()){
283 image = avatarPics.getAvatarBodyMonoImage(avatar);
285 image = avatarPics.getAvatarFaceMonoImage(avatar);
289 image = avatarPics.getGraveBodyImage();
291 image = avatarPics.getGraveImage();
296 image = avatarPics.getAvatarBodyImage(avatar);
298 image = avatarPics.getAvatarFaceImage(avatar);
309 private CharSequence getCaptionString(){
310 StringBuilder result = new StringBuilder();
312 Avatar avatar = this.talk.getAvatar();
314 if(this.talk.hasTalkNo()){
315 result.append(this.talk.getAnchorNotation_G()).append(' ');
317 result.append(avatar.getFullName()).append(' ');
318 result.append(this.talk.getAnchorNotation());
322 DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
324 long epoch = this.talk.getTimeFromID();
325 String decoded = dform.format(epoch);
326 result.append(decoded);
328 int count = this.talk.getTalkCount();
330 TalkType type = this.talk.getTalkType();
331 result.append(" (").append(Talk.encodeColorName(type));
332 result.append('#').append(count).append(')');
335 int charNum = this.talk.getTotalChars();
337 result.append(' ').append(charNum).append('字');
347 protected Color getTalkBgColor(){
348 if(this.dialogPref.isSimpleMode()) return COLOR_SIMPLEBG;
350 TalkType type = this.talk.getTalkType();
351 Color result = getTypedColor(type);
358 * @return {@inheritDoc}
361 public Rectangle recalcBounds(){
362 int newWidth = getWidth();
366 if( ! this.dialogPref.isSimpleMode()){
367 imageWidth = this.faceImage.getWidth(null);
368 imageHeight = this.faceImage.getHeight(null);
371 int tipWidth = BALOON_WOLFONLY.getWidth();
374 int minWidth = imageWidth + tipWidth + BALOON_R * 2;
375 if(newWidth < minWidth) modWidth = minWidth;
376 else modWidth = newWidth;
378 this.caption.setWidth(modWidth);
379 int captionWidth = this.caption.getWidth();
380 int captionHeight = this.caption.getHeight() + CAPTION_DIALOG_GAP;
382 this.dialog.setWidth(modWidth - minWidth);
383 int dialogWidth = this.dialog.getWidth();
384 int dialogHeight = this.dialog.getHeight();
386 if(this.dialogPref.alignBaloonWidth()){
387 this.baloonWidth = (modWidth - minWidth) + BALOON_R * 2;
389 this.baloonWidth = dialogWidth + BALOON_R * 2;
391 this.baloonHeight = dialogHeight + BALOON_R * 2;
393 int imageAndDialogWidth = imageWidth + tipWidth + this.baloonWidth;
395 int totalWidth = Math.max(captionWidth, imageAndDialogWidth);
397 int totalHeight = captionHeight;
398 totalHeight += Math.max(imageHeight, this.baloonHeight);
400 int imageYpos = captionHeight;
401 int dialogYpos = captionHeight;
402 int tipYpos = captionHeight;
403 if(imageHeight < this.baloonHeight){
404 imageYpos += (this.baloonHeight - imageHeight) / 2;
405 tipYpos += (this.baloonHeight - BALOON_WOLFONLY.getHeight()) / 2;
406 dialogYpos += BALOON_R;
408 dialogYpos += (imageHeight - this.baloonHeight) / 2 + BALOON_R;
409 tipYpos += (imageHeight - BALOON_WOLFONLY.getHeight()) / 2;
412 this.imageOrigin = new Point(0, imageYpos);
413 this.caption.setPos(this.bounds.x + 0, this.bounds.y + 0);
415 new Point(imageWidth+tipWidth+BALOON_R, dialogYpos);
416 this.dialog.setPos(this.bounds.x + imageWidth+tipWidth+BALOON_R,
417 this.bounds.y + dialogYpos);
418 this.tipOrigin = new Point(imageWidth, tipYpos);
420 for(AnchorDraw anchorDraw : this.anchorTalks){
421 anchorDraw.setWidth(modWidth - OFFSET_ANCHOR);
422 totalHeight += anchorDraw.getHeight();
425 if( this.dialogPref.isSimpleMode()
426 || this.dialogPref.alignBaloonWidth() ){
427 this.bounds.width = newWidth;
429 this.bounds.width = totalWidth;
431 this.bounds.height = UPPER_MARGIN + totalHeight + UNDER_MARGIN;
438 * @param xPos {@inheritDoc}
439 * @param yPos {@inheritDoc}
442 public void setPos(int xPos, int yPos){
443 super.setPos(xPos, yPos);
444 this.caption.setPos(this.bounds.x, this.bounds.y + UPPER_MARGIN);
445 this.dialog.setPos(this.bounds.x + this.dialogOrigin.x,
446 this.bounds.y + this.dialogOrigin.y
452 * アイコンイメージとフキダシを繋ぐ補助イメージを返す。
455 private BufferedImage getTipImage(){
458 TalkType type = this.talk.getTalkType();
460 if(this.dialogPref.isSimpleMode()){
462 case PUBLIC: tip = SQUARE_PUBLIC; break;
463 case WOLFONLY: tip = SQUARE_WOLFONLY; break;
464 case GRAVE: tip = SQUARE_GRAVE; break;
465 case PRIVATE: tip = SQUARE_PRIVATE; break;
473 case PUBLIC: tip = BALOON_PUBLIC; break;
474 case WOLFONLY: tip = BALOON_WOLFONLY; break;
475 case GRAVE: tip = BALOON_GRAVE; break;
476 case PRIVATE: tip = BALOON_PRIVATE; break;
489 * @param g {@inheritDoc}
492 public void paint(Graphics2D g){
493 final int xPos = this.bounds.x;
494 final int yPos = this.bounds.y + UPPER_MARGIN;
496 this.caption.paint(g);
498 if(this.dialogPref.isSimpleMode() ){
499 RenderingHints.Key aaHintKey = RenderingHints.KEY_ANTIALIASING;
500 Object aaHintTemp = RenderingHints.VALUE_ANTIALIAS_OFF;
501 Object aaHintOrig = g.getRenderingHint(aaHintKey);
503 RenderingHints.Key strokeHintKey =
504 RenderingHints.KEY_STROKE_CONTROL;
505 Object strokeHintTemp = RenderingHints.VALUE_STROKE_NORMALIZE;
506 Object strokeHintOrig = g.getRenderingHint(strokeHintKey);
508 g.setRenderingHint(aaHintKey, aaHintTemp);
509 g.setRenderingHint(strokeHintKey, strokeHintTemp);
511 g.drawLine(xPos, this.bounds.y,
512 xPos + this.bounds.width, this.bounds.y );
514 g.setRenderingHint(aaHintKey, aaHintOrig);
515 g.setRenderingHint(strokeHintKey, strokeHintOrig);
517 g.drawImage(this.faceImage,
518 xPos + this.imageOrigin.x,
519 yPos + this.imageOrigin.y,
523 BufferedImage tip = getTipImage();
525 xPos + this.tipOrigin.x,
526 yPos + this.tipOrigin.y,
529 g.setColor(getTalkBgColor());
531 xPos + this.dialogOrigin.x - BALOON_R,
532 yPos + this.dialogOrigin.y - BALOON_R,
538 this.dialog.paint(g);
540 int anchorX = xPos + OFFSET_ANCHOR;
541 int anchorY = yPos + this.dialogOrigin.y + this.baloonHeight;
543 for(AnchorDraw anchorDraw : this.anchorTalks){
544 anchorDraw.setPos(anchorX, anchorY);
546 anchorY += anchorDraw.getHeight();
554 * @param fontInfo {@inheritDoc}
557 public void setFontInfo(FontInfo fontInfo){
558 super.setFontInfo(fontInfo);
560 this.anchorFontInfo = deriveAnchorFontInfo(this.fontInfo);
562 this.caption.setFontInfo(this.fontInfo);
563 this.dialog .setFontInfo(this.fontInfo);
565 for(AnchorDraw anchorDraw : this.anchorTalks){
566 anchorDraw.setFontInfo(this.anchorFontInfo);
578 public void setDialogPref(DialogPref pref){
579 this.dialogPref = pref;
580 this.faceImage = getFaceImage();
582 for(AnchorDraw anchorDraw : this.anchorTalks){
583 anchorDraw.setDialogPref(this.dialogPref);
594 * @param from {@inheritDoc}
595 * @param to {@inheritDoc}
598 public void drag(Point from, Point to){
599 this.caption.drag(from, to);
600 this.dialog.drag(from, to);
601 for(AnchorDraw anchorDraw : this.anchorTalks){
602 anchorDraw.drag(from, to);
609 * @param appendable {@inheritDoc}
610 * @return {@inheritDoc}
611 * @throws java.io.IOException {@inheritDoc}
614 public Appendable appendSelected(Appendable appendable)
616 this.caption.appendSelected(appendable);
617 this.dialog .appendSelected(appendable);
619 for(AnchorDraw anchorDraw : this.anchorTalks){
620 anchorDraw.appendSelected(appendable);
630 public void clearSelect(){
631 this.caption.clearSelect();
632 this.dialog.clearSelect();
633 for(AnchorDraw anchorDraw : this.anchorTalks){
634 anchorDraw.clearSelect();
640 * 与えられた座標にアンカー文字列が存在すればAnchorを返す。
644 public Anchor getAnchor(Point pt){
645 Anchor result = this.dialog.getAnchor(pt);
651 * アンカーにnullを指定すればアンカー表示は非表示となる。
653 * @param talkList アンカーの示す一連のTalk
655 public void showAnchorTalks(Anchor anchor, List<Talk> talkList){
656 if(anchor == null || this.showingAnchor == anchor){
657 this.showingAnchor = null;
658 this.anchorTalks.clear();
663 this.showingAnchor = anchor;
665 this.anchorTalks.clear();
666 for(Talk anchorTalk : talkList){
667 AnchorDraw anchorDraw =
668 new AnchorDraw(anchorTalk,
670 this.anchorFontInfo );
671 this.anchorTalks.add(anchorDraw);
680 * 与えられた座標に検索マッチ文字列があればそのインデックスを返す。
682 * @return 検索マッチインデックス
684 public int getRegexMatchIndex(Point pt){
685 int index = this.dialog.getRegexMatchIndex(pt);
691 * @param searchRegex パターン
694 public int setRegex(Pattern searchRegex){
697 total += this.dialog.setRegex(searchRegex);
699 for(AnchorDraw anchorDraw : this.anchorTalks){
700 total += anchorDraw.setRegex(searchRegex);
709 * @return 検索ハイライトインデックス。見つからなければ-1。
711 public int getHotTargetIndex(){
712 return this.dialog.getHotTargetIndex();
717 * @param index ハイライトインデックス。負ならハイライト全クリア。
719 public void setHotTargetIndex(int index){
720 this.dialog.setHotTargetIndex(index);
728 public int getRegexMatches(){
729 return this.dialog.getRegexMatches();
733 * 特別な検索ハイライト描画をクリアする。
735 public void clearHotTarget(){
736 this.dialog.clearHotTarget();
741 * 特別な検索ハイライト領域の寸法を返す。
744 public Rectangle getHotTargetRectangle(){
745 return this.dialog.getHotTargetRectangle();