7c84f5e637aad37309be99caf68ebd26ee5f4dd0
[idea/community.git] / platform / lang-impl / src / com / intellij / codeEditor / printing / TextPainter.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.codeEditor.printing;
17
18 import com.intellij.codeInsight.daemon.LineMarkerInfo;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.editor.Editor;
21 import com.intellij.openapi.editor.RangeMarker;
22 import com.intellij.openapi.editor.ex.DocumentEx;
23 import com.intellij.openapi.editor.ex.LineIterator;
24 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
25 import com.intellij.openapi.editor.highlighter.HighlighterIterator;
26 import com.intellij.openapi.editor.markup.TextAttributes;
27 import com.intellij.openapi.fileTypes.FileType;
28 import com.intellij.openapi.progress.ProgressManager;
29 import com.intellij.openapi.project.Project;
30 import com.intellij.openapi.util.Computable;
31 import com.intellij.openapi.util.Ref;
32 import com.intellij.psi.PsiFile;
33 import com.intellij.psi.codeStyle.CodeStyleSettings;
34 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
35 import com.intellij.util.containers.IntArrayList;
36 import com.intellij.util.ui.UIUtil;
37 import org.jetbrains.annotations.NonNls;
38 import org.jetbrains.annotations.NotNull;
39
40 import java.awt.*;
41 import java.awt.font.FontRenderContext;
42 import java.awt.font.GlyphVector;
43 import java.awt.font.LineMetrics;
44 import java.awt.geom.Area;
45 import java.awt.geom.Point2D;
46 import java.awt.geom.Rectangle2D;
47 import java.awt.print.PageFormat;
48 import java.awt.print.PrinterException;
49 import java.text.SimpleDateFormat;
50 import java.util.Date;
51 import java.util.List;
52
53 class TextPainter extends BasePainter {
54   private final DocumentEx myDocument;
55   private RangeMarker myRangeToPrint;
56   private int myOffset = 0;
57   private int myLineNumber = 1;
58   private float myLineHeight = -1;
59   private float myDescent = -1;
60   private double myCharWidth = -1;
61   private final Font myPlainFont;
62   private final Font myBoldFont;
63   private final Font myItalicFont;
64   private final Font myBoldItalicFont;
65   private final Font myHeaderFont;
66   private final EditorHighlighter myHighlighter;
67   private final PrintSettings myPrintSettings;
68   private final String myFullFileName;
69   private final String myShortFileName;
70   private int myPageIndex;
71   private int myNumberOfPages = -1;
72   private int mySegmentEnd;
73   private final LineMarkerInfo[] myMethodSeparators;
74   private int myCurrentMethodSeparator;
75   private final CodeStyleSettings myCodeStyleSettings;
76   private final FileType myFileType;
77   private boolean myPerformActualDrawing;
78   
79   private final String myPrintDate;
80   private final String myPrintTime;
81
82   @NonNls private static final String DEFAULT_MEASURE_HEIGHT_TEXT = "A";
83   @NonNls private static final String DEFAULT_MEASURE_WIDTH_TEXT = "w";
84   
85   @NonNls private static final String HEADER_TOKEN_PAGE = "PAGE";
86   @NonNls private static final String HEADER_TOKEN_TOTALPAGES = "TOTALPAGES";
87   @NonNls private static final String HEADER_TOKEN_FILE = "FILE";
88   @NonNls private static final String HEADER_TOKEN_FILENAME = "FILENAME";
89   @NonNls private static final String HEADER_TOKEN_DATE = "DATE";
90   @NonNls private static final String HEADER_TOKEN_TIME = "TIME";
91   
92   @NonNls private static final String DATE_FORMAT = "yyyy-MM-dd";
93   @NonNls private static final String TIME_FORMAT = "HH:mm:ss";    
94
95   public TextPainter(@NotNull DocumentEx editorDocument,
96                      EditorHighlighter highlighter,
97                      String fullFileName,
98                      String shortFileName,
99                      @NotNull PsiFile psiFile,
100                      FileType fileType,
101                      Editor editor) {
102     this(editorDocument, highlighter, fullFileName, shortFileName, psiFile.getProject(), fileType,
103          FileSeparatorProvider.getInstance().getFileSeparators(psiFile, editorDocument, editor));
104   }
105
106   public TextPainter(@NotNull DocumentEx editorDocument,
107                      EditorHighlighter highlighter,
108                      String fullFileName,
109                      String shortFileName,
110                      Project project,
111                      FileType fileType,
112                      List<LineMarkerInfo> separators) {
113     myCodeStyleSettings = CodeStyleSettingsManager.getSettings(project);
114     myDocument = editorDocument;
115     myPrintSettings = PrintSettings.getInstance();
116     String fontName = myPrintSettings.FONT_NAME;
117     int fontSize = myPrintSettings.FONT_SIZE;
118     myPlainFont = new Font(fontName, Font.PLAIN, fontSize);
119     myBoldFont = new Font(fontName, Font.BOLD, fontSize);
120     myItalicFont = new Font(fontName, Font.ITALIC, fontSize);
121     myBoldItalicFont = new Font(fontName, Font.BOLD | Font.ITALIC, fontSize);
122     myHighlighter = highlighter;
123     myHeaderFont = new Font(myPrintSettings.FOOTER_HEADER_FONT_NAME, Font.PLAIN, myPrintSettings.FOOTER_HEADER_FONT_SIZE);
124     myFullFileName = fullFileName;
125     myShortFileName = shortFileName;
126     myRangeToPrint = editorDocument.createRangeMarker(0, myDocument.getTextLength());
127     myFileType = fileType;
128     myMethodSeparators = separators != null ? separators.toArray(new LineMarkerInfo[separators.size()]) : new LineMarkerInfo[0];
129     myCurrentMethodSeparator = 0;
130     Date date = new Date();
131     myPrintDate = new SimpleDateFormat(DATE_FORMAT).format(date);
132     myPrintTime = new SimpleDateFormat(TIME_FORMAT).format(date);
133   }
134
135   public void setSegment(int segmentStart, int segmentEnd) {
136     setSegment(myDocument.createRangeMarker(segmentStart, segmentEnd));
137   }
138
139   private void setSegment(RangeMarker marker) {
140     if (myRangeToPrint != null) {
141       myRangeToPrint.dispose();
142     }
143     myRangeToPrint = marker;
144   }
145
146   private float getLineHeight(Graphics g) {
147     if (myLineHeight >= 0) {
148       return myLineHeight;
149     }
150     FontRenderContext fontRenderContext = ((Graphics2D) g).getFontRenderContext();
151     LineMetrics lineMetrics = myPlainFont.getLineMetrics(DEFAULT_MEASURE_HEIGHT_TEXT, fontRenderContext);
152     myLineHeight = lineMetrics.getHeight();
153     return myLineHeight;
154   }
155
156   private float getDescent(Graphics g) {
157     if (myDescent >= 0) {
158       return myDescent;
159     }
160     FontRenderContext fontRenderContext = ((Graphics2D) g).getFontRenderContext();
161     LineMetrics lineMetrics = myPlainFont.getLineMetrics(DEFAULT_MEASURE_HEIGHT_TEXT, fontRenderContext);
162     myDescent = lineMetrics.getDescent();
163     return myDescent;
164   }
165
166   private Font getFont(int type) {
167     if (type == Font.BOLD)
168       return myBoldFont;
169     else if (type == Font.ITALIC)
170       return myItalicFont;
171     else if (type == Font.ITALIC + Font.BOLD)
172       return myBoldItalicFont;
173     else
174       return myPlainFont;
175   }
176
177   boolean isPrintingPass = true;
178
179   @Override
180   public int print(final Graphics g, final PageFormat pageFormat, final int pageIndex) throws PrinterException {
181     if (myProgress.isCanceled()) {
182       return NO_SUCH_PAGE;
183     }
184
185     final Graphics2D g2d = (Graphics2D)g;
186
187     if (myNumberOfPages < 0) {
188       myProgress.setText(CodeEditorBundle.message("print.file.calculating.number.of.pages.progress"));
189       
190       myPerformActualDrawing = false;
191       
192       if (!calculateNumberOfPages(g2d, pageFormat)) {
193         return NO_SUCH_PAGE;
194       }
195     }
196
197     myPerformActualDrawing = true;
198
199     return ApplicationManager.getApplication().runReadAction(new Computable<Integer>() {
200       @Override
201       public Integer compute() {
202         if (!isValidRange(myRangeToPrint)) {
203           return NO_SUCH_PAGE;
204         }
205
206         isPrintingPass = !isPrintingPass;
207         if (!isPrintingPass) {
208           return PAGE_EXISTS;
209         }
210
211         myProgress.setText(CodeEditorBundle.message("print.file.page.progress", myShortFileName, (pageIndex + 1), myNumberOfPages));
212         myPageIndex = pageIndex;
213
214         RangeMarker newRange = printPage(g2d, pageFormat, myRangeToPrint);
215         setSegment(newRange);
216
217         return PAGE_EXISTS;
218       }
219     });
220   }
221
222   private boolean calculateNumberOfPages(final Graphics2D g2d, final PageFormat pageFormat) {
223     myNumberOfPages = 0;
224     final Ref<Boolean> firstPage = new Ref<Boolean>(Boolean.TRUE);
225     final Ref<RangeMarker> tmpMarker = new Ref<RangeMarker>();
226     while (ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
227       @Override
228       public Boolean compute() {
229         if (firstPage.get()) {
230           if (!isValidRange(myRangeToPrint)) {
231             return false;
232           }
233           tmpMarker.set(myDocument.createRangeMarker(myRangeToPrint.getStartOffset(), myRangeToPrint.getEndOffset()));
234           firstPage.set(Boolean.FALSE);
235         }
236         RangeMarker range = tmpMarker.get();
237         if (!isValidRange(range)) {
238           return false;
239         }
240         tmpMarker.set(printPage(g2d, pageFormat, range));
241         range.dispose();
242         return true;
243       }
244     })) {
245       if (myProgress.isCanceled()) {
246         return false;
247       }
248       myNumberOfPages++;
249     }
250     if (!tmpMarker.isNull()) {
251       tmpMarker.get().dispose();
252     }
253     return true;
254   }
255
256   private static boolean isValidRange(RangeMarker range) {
257     return range != null && range.isValid() && range.getStartOffset() < range.getEndOffset();
258   }
259
260   /**
261    * Prints a pageful of text from a given range. Return a remaining range to print, or null if there's nothing left.
262    */
263   private RangeMarker printPage(Graphics2D g2d, PageFormat pageFormat, RangeMarker range) {
264     assert isValidRange(range);
265     int startOffset = range.getStartOffset();
266     int endOffset = range.getEndOffset();
267     
268     myOffset = startOffset;
269     mySegmentEnd = endOffset;
270     myLineNumber = myDocument.getLineNumber(myOffset) + 1;
271     Rectangle2D.Double clip = new Rectangle2D.Double(pageFormat.getImageableX(), pageFormat.getImageableY(),
272                                                      pageFormat.getImageableWidth(), pageFormat.getImageableHeight());
273     
274     draw(g2d, clip);
275
276     return myOffset > startOffset && myOffset < endOffset ? myDocument.createRangeMarker(myOffset, endOffset) : null;
277   }
278
279   private void draw(Graphics2D g2D, Rectangle2D.Double clip) {
280     double headerHeight = drawHeader(g2D, clip);
281     clip.y += headerHeight;
282     clip.height -= headerHeight;
283     double footerHeight = drawFooter(g2D, clip);
284     clip.height -= footerHeight;
285
286     Rectangle2D.Double border = (Rectangle2D.Double) clip.clone();
287     clip.x += getCharWidth(g2D) / 2;
288     clip.width -= getCharWidth(g2D);
289     if (myPrintSettings.PRINT_LINE_NUMBERS) {
290       double numbersStripWidth = calcNumbersStripWidth(g2D, clip) + getCharWidth(g2D) / 2;
291       clip.x += numbersStripWidth;
292       clip.width -= numbersStripWidth;
293     }
294     clip.x += getCharWidth(g2D) / 2;
295     clip.width -= getCharWidth(g2D);
296     drawText(g2D, clip);
297     drawBorder(g2D, border);
298   }
299
300   private void drawBorder(Graphics2D g, Rectangle2D clip) {
301     if (myPrintSettings.DRAW_BORDER && myPerformActualDrawing) {
302       Color save = g.getColor();
303       g.setColor(Color.black);
304       g.draw(clip);
305       g.setColor(save);
306     }
307   }
308
309   private double getCharWidth(Graphics2D g) {
310     if (myCharWidth < 0) {
311       FontRenderContext fontRenderContext = (g).getFontRenderContext();
312       myCharWidth = myPlainFont.getStringBounds(DEFAULT_MEASURE_WIDTH_TEXT, fontRenderContext).getWidth();
313     }
314     return myCharWidth;
315   }
316
317   private void setForegroundColor(Graphics2D g, Color color) {
318     if (color == null || !myPrintSettings.COLOR_PRINTING || !myPrintSettings.SYNTAX_PRINTING) {
319       color = Color.black;
320     }
321     g.setColor(color);
322   }
323
324   private void setBackgroundColor(Graphics2D g, Color color) {
325     if (color == null || !myPrintSettings.COLOR_PRINTING || !myPrintSettings.SYNTAX_PRINTING) {
326       color = Color.white;
327     }
328     g.setColor(color);
329   }
330
331   private void setFont(Graphics2D g, Font font) {
332     if (!myPrintSettings.SYNTAX_PRINTING) {
333       font = myPlainFont;
334     }
335     g.setFont(font);
336   }
337
338   private void drawText(Graphics2D g, Rectangle2D clip) {
339     float lineHeight = getLineHeight(g);
340     HighlighterIterator hIterator = myHighlighter.createIterator(myOffset);
341     if (hIterator.atEnd()) {
342       myOffset = mySegmentEnd;
343       return;
344     }
345     LineIterator lIterator = myDocument.createLineIterator();
346     lIterator.start(myOffset);
347     if (lIterator.atEnd()) {
348       myOffset = mySegmentEnd;
349       return;
350     }
351     TextAttributes attributes = hIterator.getTextAttributes();
352     Color currentColor = attributes.getForegroundColor();
353     Color backColor = attributes.getBackgroundColor();
354     Color underscoredColor = attributes.getEffectColor();
355     Font currentFont = getFont(attributes.getFontType());
356     setForegroundColor(g, currentColor);
357     setFont(g, currentFont);
358     g.translate(clip.getX(), 0);
359     Point2D position = new Point2D.Double(0, clip.getY());
360     double lineY = position.getY();
361
362     if (myPerformActualDrawing) {
363       setInitialMethodSeparatorIndex(lIterator.getEnd());
364     }
365
366     while (!hIterator.atEnd() && !lIterator.atEnd()) {
367       int hEnd = hIterator.getEnd();
368       int lEnd = lIterator.getEnd();
369       int lStart = lIterator.getStart();
370       if (hEnd >= lEnd) {
371         if (!drawString(g, lEnd - lIterator.getSeparatorLength(), lEnd - lStart, position, clip, backColor,
372                         underscoredColor)) {
373           drawLineNumber(g, 0, lineY);
374           break;
375         }
376         drawLineNumber(g, 0, lineY);
377         lIterator.advance();
378         myLineNumber++;
379
380         if (myPerformActualDrawing) {
381           LineMarkerInfo marker = getMethodSeparator(lEnd);
382           if (marker != null) {
383             Color save = g.getColor();
384             setForegroundColor(g, marker.separatorColor);
385             UIUtil.drawLine(g, 0, (int)lineY, (int)clip.getWidth(), (int)lineY);
386             setForegroundColor(g, save);
387           }
388         }
389
390         position.setLocation(0, position.getY() + lineHeight);
391         lineY = position.getY();
392         myOffset = lEnd;
393         if (position.getY() > clip.getY() + clip.getHeight() - lineHeight) {
394           break;
395         }
396       } else {
397         if (hEnd > lEnd - lIterator.getSeparatorLength()) {
398           if (!drawString(g, lEnd - lIterator.getSeparatorLength(), lEnd - lStart, position, clip, backColor,
399                           underscoredColor)) {
400             drawLineNumber(g, 0, lineY);
401             break;
402           }
403         } else {
404           if (!drawString(g, hEnd, lEnd - lStart, position, clip, backColor, underscoredColor)) {
405             drawLineNumber(g, 0, lineY);
406             break;
407           }
408         }
409         hIterator.advance();
410         attributes = hIterator.getTextAttributes();
411         Color color = attributes.getForegroundColor();
412         if (color == null) {
413           color = Color.black;
414         }
415         if (color != currentColor) {
416           setForegroundColor(g, color);
417           currentColor = color;
418         }
419         backColor = attributes.getBackgroundColor();
420         underscoredColor = attributes.getEffectColor();
421         Font font = getFont(attributes.getFontType());
422         if (font != currentFont) {
423           setFont(g, font);
424           currentFont = font;
425         }
426         myOffset = hEnd;
427       }
428     }
429
430     g.translate(-clip.getX(), 0);
431   }
432   
433   private void setInitialMethodSeparatorIndex(int initialOffset) {
434     while (myCurrentMethodSeparator < myMethodSeparators.length) {
435       LineMarkerInfo marker = myMethodSeparators[myCurrentMethodSeparator];
436       if (marker != null && marker.startOffset >= initialOffset) break;
437       myCurrentMethodSeparator++;
438     }
439   }
440
441   private LineMarkerInfo getMethodSeparator(int currentOffset) {
442     if (myCurrentMethodSeparator < myMethodSeparators.length) {
443       LineMarkerInfo marker = myMethodSeparators[myCurrentMethodSeparator];
444       if (marker != null && marker.startOffset < currentOffset) {
445         myCurrentMethodSeparator++;
446         return marker;
447       }
448     }
449     return null;
450   }
451
452   private double drawHeader(Graphics2D g, Rectangle2D clip) {
453     LineMetrics lineMetrics = getHeaderFooterLineMetrics(g);
454     double w = clip.getWidth();
455     double x = clip.getX();
456     double y = clip.getY();
457     double h = 0;
458     boolean wasDrawn = false;
459
460     String headerText1 = myPrintSettings.FOOTER_HEADER_TEXT1;
461     if (headerText1 != null && headerText1.length() > 0 &&
462         PrintSettings.HEADER.equals(myPrintSettings.FOOTER_HEADER_PLACEMENT1)) {
463       h = drawHeaderOrFooterLine(g, x, y, w, headerText1, myPrintSettings.FOOTER_HEADER_ALIGNMENT1);
464       wasDrawn = true;
465       y += h;
466     }
467
468     String headerText2 = myPrintSettings.FOOTER_HEADER_TEXT2;
469     if (headerText2 != null && headerText2.length() > 0 &&
470         PrintSettings.HEADER.equals(myPrintSettings.FOOTER_HEADER_PLACEMENT2)) {
471       if (PrintSettings.LEFT.equals(myPrintSettings.FOOTER_HEADER_ALIGNMENT1) &&
472           PrintSettings.RIGHT.equals(myPrintSettings.FOOTER_HEADER_ALIGNMENT2) &&
473           wasDrawn) {
474         y -= h;
475       }
476       h = drawHeaderOrFooterLine(g, x, y, w, headerText2, myPrintSettings.FOOTER_HEADER_ALIGNMENT2);
477       y += h;
478       wasDrawn = true;
479     }
480     return wasDrawn ? y - clip.getY() + lineMetrics.getHeight() / 3 : 0;
481   }
482
483   private double drawFooter(Graphics2D g, Rectangle2D clip) {
484     LineMetrics lineMetrics = getHeaderFooterLineMetrics(g);
485     double w = clip.getWidth();
486     double x = clip.getX();
487     double y = clip.getY() + clip.getHeight();
488     boolean wasDrawn = false;
489     double h = 0;
490     y -= lineMetrics.getHeight();
491     String headerText2 = myPrintSettings.FOOTER_HEADER_TEXT2;
492     if (headerText2 != null && headerText2.length() > 0 &&
493         PrintSettings.FOOTER.equals(myPrintSettings.FOOTER_HEADER_PLACEMENT2)) {
494       h = drawHeaderOrFooterLine(g, x, y, w, headerText2, myPrintSettings.FOOTER_HEADER_ALIGNMENT2);
495       wasDrawn = true;
496     }
497
498     String headerText1 = myPrintSettings.FOOTER_HEADER_TEXT1;
499     if (headerText1 != null && headerText1.length() > 0 &&
500         PrintSettings.FOOTER.equals(myPrintSettings.FOOTER_HEADER_PLACEMENT1)) {
501       y -= lineMetrics.getHeight();
502       if (PrintSettings.LEFT.equals(myPrintSettings.FOOTER_HEADER_ALIGNMENT1) &&
503           PrintSettings.RIGHT.equals(myPrintSettings.FOOTER_HEADER_ALIGNMENT2) &&
504           wasDrawn) {
505         y += h;
506       }
507       drawHeaderOrFooterLine(g, x, y, w, headerText1, myPrintSettings.FOOTER_HEADER_ALIGNMENT1);
508       wasDrawn = true;
509     }
510     return wasDrawn ? clip.getY() + clip.getHeight() - y + lineMetrics.getHeight() / 4 : 0;
511   }
512
513   private double drawHeaderOrFooterLine(Graphics2D g, double x, double y, double w, String headerText,
514                                         String alignment) {
515     FontRenderContext fontRenderContext = g.getFontRenderContext();
516     LineMetrics lineMetrics = getHeaderFooterLineMetrics(g);
517     float lineHeight = lineMetrics.getHeight();
518     if (myPerformActualDrawing) {
519       headerText = convertHeaderText(headerText);
520       g.setFont(myHeaderFont);
521       g.setColor(Color.black);
522       float descent = lineMetrics.getDescent();
523       double width = myHeaderFont.getStringBounds(headerText, fontRenderContext).getWidth() + getCharWidth(g);
524       float yPos = (float) (lineHeight - descent + y);
525       if (PrintSettings.LEFT.equals(alignment)) {
526         drawStringToGraphics(g, headerText, x, yPos);
527       } else if (PrintSettings.CENTER.equals(alignment)) {
528         drawStringToGraphics(g, headerText, (float) (x + (w - width) / 2), yPos);
529       } else if (PrintSettings.RIGHT.equals(alignment)) {
530         drawStringToGraphics(g, headerText, (float) (x + w - width), yPos);
531       }
532     }
533     return lineHeight;
534   }
535
536   private String convertHeaderText(String s) {
537     StringBuilder result = new StringBuilder("");
538     int start = 0;
539     boolean isExpression = false;
540     for (int i = 0; i < s.length(); i++) {
541       char c = s.charAt(i);
542       if (c == '$') {
543         String token = s.substring(start, i);
544         if (isExpression) {
545           if (HEADER_TOKEN_PAGE.equals(token)) {
546             result.append(myPageIndex + 1);
547           } else if (HEADER_TOKEN_TOTALPAGES.equals(token)) {
548             result.append(myNumberOfPages);
549           } else if (HEADER_TOKEN_FILE.equals(token)) {
550             result.append(myFullFileName);
551           } else if (HEADER_TOKEN_FILENAME.equals(token)) {
552             result.append(myShortFileName);
553           } else if (HEADER_TOKEN_DATE.equals(token)) {
554             result.append(myPrintDate);
555           } else if (HEADER_TOKEN_TIME.equals(token)) {
556             result.append(myPrintTime);
557           }
558         } else {
559           result.append(token);
560         }
561         isExpression = !isExpression;
562         start = i + 1;
563       }
564     }
565     if (!isExpression && start < s.length()) {
566       result.append(s.substring(start, s.length()));
567     }
568     return result.toString();
569   }
570
571   private LineMetrics getHeaderFooterLineMetrics(Graphics2D g) {
572     FontRenderContext fontRenderContext = g.getFontRenderContext();
573     return myHeaderFont.getLineMetrics(DEFAULT_MEASURE_HEIGHT_TEXT, fontRenderContext);
574   }
575
576   private double calcNumbersStripWidth(Graphics2D g, Rectangle2D clip) {
577     if (!myPrintSettings.PRINT_LINE_NUMBERS) {
578       return 0;
579     }
580     int maxLineNumber = myLineNumber + (int) (clip.getHeight() / getLineHeight(g));
581     FontRenderContext fontRenderContext = (g).getFontRenderContext();
582     double numbersStripWidth = 0;
583     for (int i = myLineNumber; i < maxLineNumber; i++) {
584       double width = myPlainFont.getStringBounds(String.valueOf(i), fontRenderContext).getWidth();
585       if (numbersStripWidth < width) {
586         numbersStripWidth = width;
587       }
588     }
589     return numbersStripWidth;
590   }
591
592   private void drawLineNumber(Graphics2D g, double x, double y) {
593     if (!myPrintSettings.PRINT_LINE_NUMBERS || !myPerformActualDrawing) {
594       return;
595     }
596     FontRenderContext fontRenderContext = (g).getFontRenderContext();
597     double width = myPlainFont.getStringBounds(String.valueOf(myLineNumber), fontRenderContext).getWidth() + getCharWidth(g);
598     Color savedColor = g.getColor();
599     Font savedFont = g.getFont();
600     g.setColor(Color.black);
601     g.setFont(myPlainFont);
602     drawStringToGraphics(g, String.valueOf(myLineNumber), x - width, getLineHeight(g) - getDescent(g) + y);
603     g.setColor(savedColor);
604     g.setFont(savedFont);
605   }
606
607   private boolean drawString(Graphics2D g, int end, int colNumber, Point2D position, Rectangle2D clip, Color backColor,
608                              Color underscoredColor) {
609     ProgressManager.checkCanceled();
610     if (myOffset >= end)
611       return true;
612     char[] text = myDocument.getCharsSequence().toString().toCharArray(); //TODO: Make drawTabbedString work with CharSequence instead.
613     boolean isInClip = (getLineHeight(g) + position.getY() >= clip.getY()) &&
614                        (position.getY() <= clip.getY() + clip.getHeight());
615     if (!isInClip)
616       return true;
617     return drawTabbedString(g, text, end - myOffset, position, clip, colNumber, backColor, underscoredColor);
618   }
619
620   private boolean drawTabbedString(final Graphics2D g, char[] text, int length, Point2D position, Rectangle2D clip,
621                                    int colNumber, Color backColor, Color underscoredColor) {
622     boolean ret = true;
623     if (myOffset + length >= mySegmentEnd) {
624       ret = false;
625       length = mySegmentEnd - myOffset;
626     }
627     if (length <= 0) { // can happen in recursive invocations below
628       return false;
629     }
630     if (myPrintSettings.WRAP) {
631       double w = getTextSegmentWidth(text, myOffset, length, position.getX(), g);
632       if (position.getX() + w > clip.getWidth()) {
633         IntArrayList breakOffsets = LineWrapper.calcBreakOffsets(text, myOffset, myOffset + length, colNumber, position.getX(),
634                                                                     clip.getWidth(), new LineWrapper.WidthProvider() {
635           @Override
636           public double getWidth(char[] text, int start, int count, double x) {
637             return getTextSegmentWidth(text, start, count, x, g);
638           }
639         });
640         int startOffset = myOffset;
641         for (int i = 0; i < breakOffsets.size(); i++) {
642           int breakOffset = breakOffsets.get(i);
643           drawTabbedString(g, text, breakOffset - myOffset, position, clip, colNumber, backColor, underscoredColor);
644           position.setLocation(0, position.getY() + getLineHeight(g));
645           if (position.getY() > clip.getY() + clip.getHeight() - getLineHeight(g)) {
646             return false;
647           }
648         }
649         drawTabbedString(g, text, startOffset + length - myOffset, position, clip, colNumber, backColor, underscoredColor);
650         return ret;
651       }
652     }
653     double xStart = position.getX();
654     double x = position.getX();
655     double y = getLineHeight(g) - getDescent(g) + position.getY();
656     if (backColor != null && myPerformActualDrawing) {
657       Color savedColor = g.getColor();
658       setBackgroundColor(g, backColor);
659       double w = getTextSegmentWidth(text, myOffset, length, position.getX(), g);
660       g.fill(new Area(new Rectangle2D.Double(position.getX(),
661                                              y - getLineHeight(g) + getDescent(g),
662                                              w,
663                                              getLineHeight(g))));
664       g.setColor(savedColor);
665     }
666
667     int start = myOffset;
668
669     for (int i = myOffset; i < myOffset + length; i++) {
670       if (text[i] != '\t')
671         continue;
672       if (i > start) {
673         String s = new String(text, start, i - start);
674         x += drawStringToGraphics(g, s, x, y);
675       }
676       x = nextTabStop(g, x);
677       start = i + 1;
678     }
679
680     if (myOffset + length > start) {
681       String s = new String(text, start, myOffset + length - start);
682       x += drawStringToGraphics(g, s, x, y);
683     }
684
685     if (underscoredColor != null && myPerformActualDrawing) {
686       Color savedColor = g.getColor();
687       setForegroundColor(g, underscoredColor);
688       double w = getTextSegmentWidth(text, myOffset, length, position.getX(), g);
689       UIUtil.drawLine(g, (int)position.getX(), (int)y + 1, (int)(xStart + w), (int)(y + 1));
690       g.setColor(savedColor);
691     }
692     position.setLocation(x, position.getY());
693     myOffset += length;
694     return ret;
695   }
696
697   private double drawStringToGraphics(Graphics2D g, String s, double x, double y) {
698     if (!myPrintSettings.PRINT_AS_GRAPHICS) {
699       if (myPerformActualDrawing) {
700         g.drawString(s, (float)x, (float)y);
701       }
702       return g.getFontMetrics().stringWidth(s);
703     } else {
704       GlyphVector v = g.getFont().createGlyphVector(g.getFontRenderContext(), s);
705       if (myPerformActualDrawing) {
706         g.translate(x, y);
707         g.fill(v.getOutline());
708         g.translate(-x, -y);
709       }
710
711       return v.getLogicalBounds().getWidth();
712     }
713   }
714   private double getTextSegmentWidth(char[] text, int offset, int length, double x, Graphics2D g) {
715     int start = offset;
716     double startX = x;
717
718     for (int i = offset; i < offset + length; i++) {
719       if (text[i] != '\t')
720         continue;
721
722       if (i > start) {
723         x += getStringWidth(g, text, start, i - start);
724       }
725       x = nextTabStop(g, x);
726       start = i + 1;
727     }
728
729     if (offset + length > start) {
730       x += getStringWidth(g, text, start, offset + length - start);
731     }
732
733     return x - startX;
734   }
735
736   private static double getStringWidth(Graphics2D g, char[] text, int start, int count) {
737     String s = new String(text, start, count);
738     GlyphVector v = g.getFont().createGlyphVector(g.getFontRenderContext(), s);
739
740     return v.getLogicalBounds().getWidth();
741   }
742
743   public double nextTabStop(Graphics2D g, double x) {
744     double tabSize = myCodeStyleSettings.getTabSize(myFileType);
745     if (tabSize <= 0) {
746       tabSize = 1;
747     }
748
749     tabSize *= g.getFont().getStringBounds(" ", g.getFontRenderContext()).getWidth();
750
751     int nTabs = (int) (x / tabSize);
752     return (nTabs + 1) * tabSize;
753   }
754
755   @Override
756   void dispose() {
757     setSegment(null);
758   }
759 }