4 * Copyright(c) 2008 olyutorskii
\r
5 * $Id: GlyphDraw.java 959 2009-12-14 14:11:01Z olyutorskii $
\r
8 package jp.sourceforge.jindolf;
\r
10 import java.awt.Color;
\r
11 import java.awt.FontMetrics;
\r
12 import java.awt.Graphics2D;
\r
13 import java.awt.Point;
\r
14 import java.awt.Rectangle;
\r
15 import java.awt.Shape;
\r
16 import java.awt.font.GlyphVector;
\r
17 import java.awt.geom.Rectangle2D;
\r
18 import java.io.IOException;
\r
19 import java.text.CharacterIterator;
\r
20 import java.util.Collection;
\r
21 import java.util.LinkedList;
\r
22 import java.util.List;
\r
23 import java.util.regex.Matcher;
\r
24 import java.util.regex.Pattern;
\r
25 import javax.swing.SwingConstants;
\r
31 public class GlyphDraw extends AbstractTextRow implements SwingConstants{
\r
33 private static final Color COLOR_SELECTION = new Color(0xb8cfe5);
\r
34 private static final Color COLOR_SEARCHHIT = new Color(0xb2b300);
\r
35 private static final Color COLOR_HOTTARGET = Color.ORANGE;
\r
37 private Color foregroundColor = Color.WHITE;
\r
38 private final CharSequence source;
\r
40 private float[] dimArray;
\r
41 private final List<GlyphVector> lines = new LinkedList<GlyphVector>();
\r
42 private Collection<Anchor> anchorSet;
\r
43 private final List<MatchInfo> matchList = new LinkedList<MatchInfo>();
\r
44 private MatchInfo hotTarget = null;
\r
46 private int selectStart = -1;
\r
47 private int selectLast = -1;
\r
53 public GlyphDraw(CharSequence source){
\r
54 this(source, FontInfo.DEFAULT_FONTINFO);
\r
61 * @param fontInfo フォント設定
\r
63 public GlyphDraw(CharSequence source, FontInfo fontInfo){
\r
66 this.source = source;
\r
68 GlyphVector gv = createGlyphVector(this.source);
\r
70 int sourceLength = gv.getNumGlyphs();
\r
72 this.dimArray = gv.getGlyphPositions(0, sourceLength+1, null);
\r
81 public Color getColor(){
\r
82 return this.foregroundColor;
\r
89 public void setColor(Color color){
\r
90 this.foregroundColor = color;
\r
96 * アンカーの位置指定はコンストラクタに与えた文字列に対するものでなければ
\r
98 * @param anchorSet アンカーの集合
\r
100 public void setAnchorSet(Collection<Anchor> anchorSet){
\r
101 this.anchorSet = anchorSet;
\r
107 * @param fromPos 文字列開始位置
\r
108 * @param toPos 文字列終了位置
\r
111 public float getSpan(int fromPos, int toPos){
\r
112 float from = this.dimArray[fromPos * 2];
\r
113 float to = this.dimArray[(toPos+1) * 2];
\r
114 float span = to - from;
\r
119 * 指定領域の文字列から行情報を生成し内部に登録する。
\r
120 * @param from 文字列開始位置
\r
121 * @param to 文字列終了位置
\r
124 protected GlyphVector createLine(int from, int to){
\r
125 GlyphVector line = createGlyphVector(this.source, from, to + 1);
\r
126 this.lines.add(line);
\r
132 * @return {@inheritDoc}
\r
134 // TODO 最後が \n で終わるダイアログが無限再帰を起こす?
\r
135 public Rectangle recalcBounds(){
\r
136 float newWidth = (float) getWidth();
\r
137 this.lines.clear();
\r
138 CharacterIterator iterator;
\r
139 iterator = new SequenceCharacterIterator(this.source);
\r
140 int from = iterator.getIndex();
\r
143 char ch = iterator.current();
\r
145 if(ch == CharacterIterator.DONE){
\r
147 createLine(from, to - 1);
\r
153 createLine(from, to);
\r
160 float fwidth = getSpan(from, to);
\r
161 if(fwidth > newWidth){
\r
163 createLine(from, to - 1);
\r
166 createLine(from, to);
\r
178 int totalWidth = 0;
\r
179 int totalHeight = 0;
\r
180 for(GlyphVector gv : this.lines){
\r
181 Rectangle2D r2d = gv.getLogicalBounds();
\r
182 Rectangle rect = r2d.getBounds();
\r
183 totalWidth = Math.max(totalWidth, rect.width);
\r
184 totalHeight += rect.height;
\r
187 this.bounds.width = totalWidth;
\r
188 this.bounds.height = totalHeight;
\r
190 return this.bounds;
\r
195 * @param fontInfo {@inheritDoc}
\r
198 public void setFontInfo(FontInfo fontInfo){
\r
199 super.setFontInfo(fontInfo);
\r
201 GlyphVector gv = createGlyphVector(this.source);
\r
203 int sourceLength = gv.getNumGlyphs();
\r
205 this.dimArray = gv.getGlyphPositions(0, sourceLength+1, null);
\r
213 * 指定された点座標が文字列のどこを示すか判定する。
\r
215 * @return 文字位置。座標が文字列以外を示す場合は-1を返す。
\r
217 public int getCharIndex(Point pt){
\r
218 if( ! this.bounds.contains(pt) ) return -1;
\r
221 int xPos = this.bounds.x;
\r
222 int yPos = this.bounds.y;
\r
223 for(GlyphVector gv : this.lines){
\r
224 Rectangle2D r2d = gv.getLogicalBounds();
\r
225 Rectangle rect = r2d.getBounds();
\r
228 int sourceLength = gv.getNumGlyphs();
\r
229 if(rect.contains(pt)){
\r
230 for(int pos = 0; pos < sourceLength; pos++){
\r
231 float span = getSpan(sPos, sPos+pos);
\r
232 if(span+xPos > pt.x) return sPos + pos;
\r
236 yPos += rect.height;
\r
237 sPos += sourceLength;
\r
245 * @param appendable {@inheritDoc}
\r
246 * @return {@inheritDoc}
\r
247 * @throws java.io.IOException {@inheritDoc}
\r
249 public Appendable appendSelected(Appendable appendable)
\r
250 throws IOException{
\r
251 if(this.selectStart < 0 || this.selectLast < 0) return appendable;
\r
252 CharSequence subsel;
\r
253 subsel = this.source.subSequence(this.selectStart,
\r
254 this.selectLast + 1);
\r
255 appendable.append(subsel);
\r
262 public void clearSelect(){
\r
263 this.selectStart = -1;
\r
264 this.selectLast = -1;
\r
269 * 指定した部分文字列を選択された状態にする。
\r
270 * @param start 文字列開始位置
\r
271 * @param last 文字列終了位置
\r
273 public void select(int start, int last){
\r
275 this.selectStart = start;
\r
276 this.selectLast = last;
\r
278 this.selectStart = last;
\r
279 this.selectLast = start;
\r
281 this.selectLast = Math.min(this.source.length() - 1,
\r
288 * @param from {@inheritDoc}
\r
289 * @param to {@inheritDoc}
\r
291 public void drag(Point from, Point to){
\r
292 Point fromPt = from;
\r
294 if(fromPt.y > toPt.y || (fromPt.y == toPt.y && fromPt.x > toPt.x)){
\r
295 Point swapPt = fromPt;
\r
300 int fromDirection = GUIUtils.getDirection(this.bounds, fromPt);
\r
301 int toDirection = GUIUtils.getDirection(this.bounds, toPt);
\r
303 if(fromDirection == toDirection){
\r
304 if( fromDirection == NORTH
\r
305 || fromDirection == SOUTH){
\r
311 int fromIndex = -1;
\r
314 if(fromDirection == NORTH){
\r
317 if(toDirection == SOUTH){
\r
318 toIndex = this.source.length() - 1;
\r
322 fromIndex = getCharIndex(fromPt);
\r
325 toIndex = getCharIndex(toPt);
\r
328 if(fromIndex >= 0 && toIndex >= 0){
\r
329 select(fromIndex, toIndex);
\r
333 int xPos = this.bounds.x;
\r
334 int yPos = this.bounds.y;
\r
336 for(GlyphVector gv : this.lines){
\r
337 int glyphStart = accumPos;
\r
338 int glyphLast = accumPos + gv.getNumGlyphs() - 1;
\r
339 Rectangle2D r2d = gv.getLogicalBounds();
\r
340 Rectangle rect = r2d.getBounds();
\r
345 && GUIUtils.getDirection(rect, fromPt) == SOUTH){
\r
346 yPos += rect.height;
\r
347 accumPos = glyphLast + 1;
\r
349 }else if( toIndex < 0
\r
350 && GUIUtils.getDirection(rect, toPt) == NORTH){
\r
355 int dir = GUIUtils.getDirection(rect, fromPt);
\r
357 fromIndex = glyphStart;
\r
358 }else if(dir == WEST){
\r
359 fromIndex = glyphLast+1;
\r
363 int dir = GUIUtils.getDirection(rect, toPt);
\r
365 toIndex = glyphStart - 1;
\r
366 }else if(dir == WEST){
\r
367 toIndex = glyphLast;
\r
371 if(fromIndex >= 0 && toIndex >= 0){
\r
372 select(fromIndex, toIndex);
\r
376 yPos += rect.height;
\r
377 accumPos = glyphLast + 1;
\r
385 * 文字列検索がヒットした箇所のハイライト描画を行う。
\r
386 * @param g グラフィックスコンテキスト
\r
388 private void paintRegexHitted(Graphics2D g){
\r
389 if(this.matchList.size() <= 0) return;
\r
391 FontMetrics metrics = g.getFontMetrics();
\r
392 final int ascent = metrics.getAscent();
\r
394 int xPos = this.bounds.x;
\r
395 int yPos = this.bounds.y + ascent;
\r
399 for(GlyphVector line : this.lines){
\r
400 int glyphStart = accumPos;
\r
401 int glyphLast = accumPos + line.getNumGlyphs() - 1;
\r
403 for(MatchInfo match : this.matchList){
\r
404 int matchStart = match.getStartPos();
\r
405 int matchLast = match.getEndPos() - 1;
\r
407 if(matchLast < glyphStart) continue;
\r
408 if(glyphLast < matchStart) break;
\r
410 int hilightStart = Math.max(matchStart, glyphStart);
\r
411 int hilightLast = Math.min(matchLast, glyphLast);
\r
413 shape = line.getGlyphLogicalBounds(hilightStart - glyphStart);
\r
414 Rectangle hilight = shape.getBounds();
\r
415 shape = line.getGlyphLogicalBounds(hilightLast - glyphStart);
\r
416 hilight.add(shape.getBounds());
\r
418 if(match == this.hotTarget){
\r
419 g.setColor(COLOR_HOTTARGET);
\r
421 g.setColor(COLOR_SEARCHHIT);
\r
424 g.fillRect(xPos + hilight.x,
\r
430 Rectangle2D r2d = line.getLogicalBounds();
\r
431 Rectangle rect = r2d.getBounds();
\r
433 yPos += rect.height;
\r
435 accumPos = glyphLast + 1;
\r
442 * 選択文字列のハイライト描画を行う。
\r
443 * @param g グラフィックスコンテキスト
\r
445 private void paintSelected(Graphics2D g){
\r
446 if(this.selectStart < 0 || this.selectLast < 0) return;
\r
448 g.setColor(COLOR_SELECTION);
\r
450 int xPos = this.bounds.x;
\r
451 int yPos = this.bounds.y;
\r
455 for(GlyphVector line : this.lines){
\r
456 int glyphStart = accumPos;
\r
457 int glyphLast = accumPos + line.getNumGlyphs() - 1;
\r
459 if(this.selectLast < glyphStart) break;
\r
461 Rectangle2D r2d = line.getLogicalBounds();
\r
462 Rectangle rect = r2d.getBounds();
\r
464 if(glyphLast < this.selectStart){
\r
465 yPos += rect.height;
\r
466 accumPos = glyphLast + 1;
\r
470 int hilightStart = Math.max(this.selectStart, glyphStart);
\r
471 int hilightLast = Math.min(this.selectLast, glyphLast);
\r
473 shape = line.getGlyphLogicalBounds(hilightStart - glyphStart);
\r
474 Rectangle hilight = shape.getBounds();
\r
475 shape = line.getGlyphLogicalBounds(hilightLast - glyphStart);
\r
476 hilight.add(shape.getBounds());
\r
478 g.fillRect(xPos + hilight.x,
\r
483 yPos += rect.height;
\r
484 accumPos = glyphLast + 1;
\r
491 * アンカー文字列のハイライト描画を行う。
\r
492 * @param g グラフィックスコンテキスト
\r
494 private void paintAnchorBack(Graphics2D g){
\r
495 if(this.anchorSet == null) return;
\r
496 if(this.anchorSet.size() <= 0) return;
\r
498 FontMetrics metrics = g.getFontMetrics();
\r
499 final int ascent = metrics.getAscent();
\r
501 g.setColor(Color.GRAY);
\r
503 int xPos = this.bounds.x;
\r
504 int yPos = this.bounds.y + ascent;
\r
508 for(GlyphVector line : this.lines){
\r
509 int glyphStart = accumPos;
\r
510 int glyphLast = accumPos + line.getNumGlyphs() - 1;
\r
512 for(Anchor anchor : this.anchorSet){
\r
513 int anchorStart = anchor.getStartPos();
\r
514 int anchorLast = anchor.getEndPos() - 1;
\r
516 if(anchorLast < glyphStart) continue;
\r
517 if(glyphLast < anchorStart) break;
\r
519 int hilightStart = Math.max(anchorStart, glyphStart);
\r
520 int hilightLast = Math.min(anchorLast, glyphLast);
\r
522 shape = line.getGlyphLogicalBounds(hilightStart - glyphStart);
\r
523 Rectangle hilight = shape.getBounds();
\r
524 shape = line.getGlyphLogicalBounds(hilightLast - glyphStart);
\r
525 hilight.add(shape.getBounds());
\r
527 g.fillRect(xPos + hilight.x,
\r
533 Rectangle2D r2d = line.getLogicalBounds();
\r
534 Rectangle rect = r2d.getBounds();
\r
536 yPos += rect.height;
\r
538 accumPos = glyphLast + 1;
\r
546 * @param g {@inheritDoc}
\r
548 public void paint(Graphics2D g){
\r
549 g.setFont(this.fontInfo.getFont());
\r
550 FontMetrics metrics = g.getFontMetrics();
\r
551 int ascent = metrics.getAscent();
\r
553 int xPos = this.bounds.x;
\r
554 int yPos = this.bounds.y + ascent;
\r
556 paintAnchorBack(g);
\r
557 paintRegexHitted(g);
\r
560 g.setColor(this.foregroundColor);
\r
561 for(GlyphVector gv : this.lines){
\r
562 g.drawGlyphVector(gv, xPos, yPos);
\r
564 Rectangle2D r2d = gv.getLogicalBounds();
\r
565 Rectangle rect = r2d.getBounds();
\r
567 yPos += rect.height;
\r
574 * 与えられた座標にアンカー文字列が存在すればAnchorを返す。
\r
578 public Anchor getAnchor(Point pt){
\r
579 int targetIdx = getCharIndex(pt);
\r
580 if(targetIdx < 0) return null;
\r
582 for(Anchor anchor : this.anchorSet){
\r
583 int anchorStart = anchor.getStartPos();
\r
584 int anchorEnd = anchor.getEndPos();
\r
585 if(anchorStart <= targetIdx && targetIdx <= anchorEnd - 1){
\r
594 * 与えられた座標に検索マッチ文字列があればそのインデックスを返す。
\r
596 * @return 検索マッチインデックス
\r
598 public int getRegexMatchIndex(Point pt){
\r
599 int targetIdx = getCharIndex(pt);
\r
600 if(targetIdx < 0) return -1;
\r
603 for(MatchInfo info : this.matchList){
\r
604 int matchStart = info.getStartPos();
\r
605 int matchEnd = info.getEndPos();
\r
606 if(matchStart <= targetIdx && targetIdx <= matchEnd - 1){
\r
617 * @param searchRegex パターン
\r
620 public int setRegex(Pattern searchRegex){
\r
622 this.matchList.clear();
\r
623 if(searchRegex == null) return 0;
\r
625 Matcher matcher = searchRegex.matcher(this.source);
\r
626 while(matcher.find()){
\r
627 int startPos = matcher.start();
\r
628 int endPos = matcher.end();
\r
629 if(startPos >= endPos) break; // 長さ0マッチは無視
\r
630 MatchInfo matchInfo = new MatchInfo(startPos, endPos);
\r
631 this.matchList.add(matchInfo);
\r
634 return getRegexMatches();
\r
638 * 検索ハイライトインデックスを返す。
\r
639 * @return 検索ハイライトインデックス。見つからなければ-1。
\r
641 public int getHotTargetIndex(){
\r
642 return this.matchList.indexOf(this.hotTarget);
\r
647 * @param index ハイライトインデックス。負ならハイライト全クリア。
\r
649 public void setHotTargetIndex(int index){
\r
654 this.hotTarget = this.matchList.get(index);
\r
662 public int getRegexMatches(){
\r
663 return this.matchList.size();
\r
667 * 特別な検索ハイライト描画をクリアする。
\r
669 public void clearHotTarget(){
\r
670 this.hotTarget = null;
\r
675 * 特別な検索ハイライト領域の寸法を返す。
\r
676 * @return ハイライト領域寸法
\r
678 public Rectangle getHotTargetRectangle(){
\r
679 Rectangle result = null;
\r
681 if(this.hotTarget == null) return result;
\r
683 int xPos = this.bounds.x;
\r
684 int yPos = this.bounds.y;
\r
688 int matchStart = this.hotTarget.getStartPos();
\r
689 int matchLast = this.hotTarget.getEndPos() - 1;
\r
691 for(GlyphVector gv : this.lines){
\r
692 int glyphStart = accumPos;
\r
693 int glyphLast = accumPos + gv.getNumGlyphs() - 1;
\r
695 if(matchLast < glyphStart) break;
\r
697 if(matchStart <= glyphLast){
\r
698 int hilightStart = Math.max(matchStart, glyphStart);
\r
699 int hilightLast = Math.min(matchLast, glyphLast);
\r
702 shape = gv.getGlyphLogicalBounds(hilightStart - glyphStart);
\r
703 Rectangle hilight = shape.getBounds();
\r
704 shape = gv.getGlyphLogicalBounds(hilightLast - glyphStart);
\r
705 hilight.add(shape.getBounds());
\r
707 Rectangle temp = new Rectangle(xPos + hilight.x,
\r
711 if(result == null){
\r
718 Rectangle2D r2d = gv.getLogicalBounds();
\r
719 Rectangle rect = r2d.getBounds();
\r
720 yPos += rect.height;
\r
722 accumPos = glyphLast + 1;
\r
731 private static class MatchInfo{
\r
733 private final int startPos;
\r
734 private final int endPos;
\r
738 * @param startPos ヒット開始位置
\r
739 * @param endPos ヒット終了位置
\r
741 public MatchInfo(int startPos, int endPos){
\r
743 this.startPos = startPos;
\r
744 this.endPos = endPos;
\r
752 public int getStartPos(){
\r
753 return this.startPos;
\r
760 public int getEndPos(){
\r
761 return this.endPos;
\r