1 /*
2  * Copyright (c) 2008-2019 Emmanuel Dupuy.
3  * This project is distributed under the GPLv3 license.
4  * This is a Copyleft license that gives the user the right to use,
5  * copy and modify the code freely for non-commercial purposes.
6  */
7 
8 package org.jd.gui.view.component;
9 
10 import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
11 import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaEditorKit;
12 import org.fife.ui.rsyntaxtextarea.RSyntaxTextAreaUI;
13 import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
14 import org.fife.ui.rsyntaxtextarea.folding.Fold;
15 import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
16 import org.fife.ui.rtextarea.Gutter;
17 import org.fife.ui.rtextarea.LineNumberList;
18 import org.fife.ui.rtextarea.RTextArea;
19 import org.fife.ui.rtextarea.RTextAreaUI;
20 
21 import javax.swing.*;
22 import javax.swing.text.EditorKit;
23 import javax.swing.text.Element;
24 import javax.swing.text.JTextComponent;
25 import javax.swing.text.View;
26 import java.awt.*;
27 import java.util.Arrays;
28 import java.util.Map;
29 
30 public abstract class CustomLineNumbersPage extends HyperlinkPage {
31     protected Color errorForeground = Color.RED;
32     protected boolean showMisalignment = true;
33 
setErrorForeground(Color color)34     public void setErrorForeground(Color color) {
35         errorForeground = color;
36     }
37 
setShowMisalignment(boolean b)38     public void setShowMisalignment(boolean b) {
39         showMisalignment = b;
40     }
41 
42     /**
43      * Map[textarea line number] = original line number
44      */
45     protected int[] lineNumberMap = null;
46     protected int maxLineNumber = 0;
47 
setMaxLineNumber(int maxLineNumber)48     protected void setMaxLineNumber(int maxLineNumber) {
49         if (maxLineNumber > 0) {
50             if (lineNumberMap == null) {
51                 lineNumberMap = new int[maxLineNumber+1];
52             } else if (lineNumberMap.length <= maxLineNumber) {
53                 int[] tmp = new int[maxLineNumber+1];
54                 System.arraycopy(lineNumberMap, 0, tmp, 0, lineNumberMap.length);
55                 lineNumberMap = tmp;
56             }
57 
58             this.maxLineNumber = maxLineNumber;
59         }
60     }
61 
initLineNumbers()62     protected void initLineNumbers() {
63         String text = getText();
64         int len = text.length();
65 
66         if (len == 0) {
67             setMaxLineNumber(0);
68         } else {
69             int mln = len - text.replace("\n", "").length();
70 
71             if (text.charAt(len-1) != '\n') {
72                 mln++;
73             }
74 
75             setMaxLineNumber(mln);
76 
77             for (int i=1; i<=maxLineNumber; i++) {
78                 lineNumberMap[i] = i;
79             }
80         }
81     }
82 
setLineNumber(int textAreaLineNumber, int originalLineNumber)83     protected void setLineNumber(int textAreaLineNumber, int originalLineNumber) {
84         if (originalLineNumber > 0) {
85             setMaxLineNumber(textAreaLineNumber);
86             lineNumberMap[textAreaLineNumber] = originalLineNumber;
87         }
88     }
89 
clearLineNumbers()90     protected void clearLineNumbers() {
91         if (lineNumberMap != null) {
92             Arrays.fill(lineNumberMap, 0);
93         }
94     }
95 
getMaximumSourceLineNumber()96     protected int getMaximumSourceLineNumber() { return maxLineNumber; }
97 
getTextAreaLineNumber(int originalLineNumber)98     protected int getTextAreaLineNumber(int originalLineNumber) {
99         int textAreaLineNumber = 1;
100         int greatestLowerSourceLineNumber = 0;
101         int i = lineNumberMap.length;
102 
103         while (i-- > 0) {
104             int sln = lineNumberMap[i];
105             if (sln <= originalLineNumber) {
106                 if (greatestLowerSourceLineNumber < sln) {
107                     greatestLowerSourceLineNumber = sln;
108                     textAreaLineNumber = i;
109                 }
110             }
111         }
112 
113         return textAreaLineNumber;
114     }
115 
newSyntaxTextArea()116     @Override protected RSyntaxTextArea newSyntaxTextArea() { return new SourceSyntaxTextArea(); }
117 
118     public class SourceSyntaxTextArea extends HyperlinkSyntaxTextArea {
createRTextAreaUI()119         @Override protected RTextAreaUI createRTextAreaUI() { return new SourceSyntaxTextAreaUI(this); }
120     }
121 
122     /**
123      * A lot of code to replace the default LineNumberList...
124      */
125     public class SourceSyntaxTextAreaUI extends RSyntaxTextAreaUI {
SourceSyntaxTextAreaUI(JComponent rSyntaxTextArea)126         public SourceSyntaxTextAreaUI(JComponent rSyntaxTextArea) { super(rSyntaxTextArea); }
getEditorKit(JTextComponent tc)127         @Override public EditorKit getEditorKit(JTextComponent tc) { return new SourceSyntaxTextAreaEditorKit(); }
getVisibleEditorRect()128         @Override public Rectangle getVisibleEditorRect() { return super.getVisibleEditorRect(); }
129     }
130 
131     public class SourceSyntaxTextAreaEditorKit extends RSyntaxTextAreaEditorKit {
createLineNumberList(RTextArea textArea)132         @Override public LineNumberList createLineNumberList(RTextArea textArea) { return new SourceLineNumberList(textArea); }
133     }
134 
135     /**
136      * Why 'LineNumberList' is so unexpandable ? Too many private fields & methods and too many package scope.
137      */
138     public class SourceLineNumberList extends LineNumberList {
139         protected RTextArea rTextArea;
140         protected Map<?,?> aaHints;
141         protected Rectangle visibleRect;
142         protected Insets textAreaInsets;
143         protected Dimension preferredSize;
144 
SourceLineNumberList(RTextArea textArea)145         public SourceLineNumberList(RTextArea textArea) {
146             super(textArea, null);
147             this.rTextArea = textArea;
148         }
149 
150         @Override
init()151         protected void init() {
152             super.init();
153             visibleRect = new Rectangle();
154             aaHints = RSyntaxUtilities.getDesktopAntiAliasHints();
155             textAreaInsets = null;
156         }
157 
158         /**
159          * @see org.fife.ui.rtextarea.LineNumberList#paintComponent(java.awt.Graphics)
160          */
161         @Override
paintComponent(Graphics g)162         protected void paintComponent(Graphics g) {
163             visibleRect = g.getClipBounds(visibleRect);
164 
165             if (visibleRect == null) {
166                 visibleRect = getVisibleRect();
167             }
168             if (visibleRect == null) {
169                 return;
170             }
171 
172             int cellWidth = getPreferredSize().width;
173             int cellHeight = rTextArea.getLineHeight();
174             int ascent = rTextArea.getMaxAscent();
175             FoldManager fm = ((RSyntaxTextArea)rTextArea).getFoldManager();
176             int RHS_BORDER_WIDTH = getRhsBorderWidth();
177             FontMetrics metrics = g.getFontMetrics();
178             int rhs = getWidth() - RHS_BORDER_WIDTH;
179 
180             if (getParent() instanceof Gutter) { // Should always be true
181                 g.setColor(getParent().getBackground());
182             } else {
183                 g.setColor(getBackground());
184             }
185 
186             g.fillRect(0, visibleRect.y, cellWidth, visibleRect.height);
187             g.setFont(getFont());
188 
189             if (aaHints != null) {
190                 ((Graphics2D)g).addRenderingHints(aaHints);
191             }
192 
193             if (rTextArea.getLineWrap()) {
194                 SourceSyntaxTextAreaUI ui = (SourceSyntaxTextAreaUI)rTextArea.getUI();
195                 View v = ui.getRootView(rTextArea).getView(0);
196                 Element root = rTextArea.getDocument().getDefaultRootElement();
197                 int lineCount = root.getElementCount();
198                 int topPosition = rTextArea.viewToModel(visibleRect.getLocation());
199                 int topLine = root.getElementIndex(topPosition);
200                 Rectangle visibleEditorRect = ui.getVisibleEditorRect();
201                 Rectangle r = LineNumberList.getChildViewBounds(v, topLine, visibleEditorRect);
202                 int y = r.y;
203 
204                 int visibleBottom =  visibleRect.y + visibleRect.height;
205 
206                 // Keep painting lines until our y-coordinate is past the visible
207                 // end of the text area.
208 
209                 while (y < visibleBottom) {
210                     r = getChildViewBounds(v, topLine, visibleEditorRect);
211 
212                     // Paint the line number.
213                     paintLineNumber(g, metrics, rhs, y+ascent, topLine + 1);
214 
215                     // The next possible y-coordinate is just after the last line
216                     // painted.
217                     y += r.height;
218 
219                     // Update topLine (we're actually using it for our "current line"
220                     // variable now).
221                     if (fm != null) {
222                         Fold fold = fm.getFoldForLine(topLine);
223                         if ((fold != null) && fold.isCollapsed()) {
224                             topLine += fold.getCollapsedLineCount();
225                         }
226                     }
227 
228                     if (++topLine >= lineCount) {
229                         break;
230                     }
231                 }
232             } else {
233                 textAreaInsets = rTextArea.getInsets(textAreaInsets);
234 
235                 if (visibleRect.y < textAreaInsets.top) {
236                     visibleRect.height -= (textAreaInsets.top - visibleRect.y);
237                     visibleRect.y = textAreaInsets.top;
238                 }
239 
240                 int topLine = (visibleRect.y - textAreaInsets.top) / cellHeight;
241                 int actualTopY = topLine * cellHeight + textAreaInsets.top;
242                 int y = actualTopY + ascent;
243 
244                 // Get the actual first line to paint, taking into account folding.
245                 topLine += fm.getHiddenLineCountAbove(topLine, true);
246 
247                 // Paint line numbers
248                 g.setColor(getForeground());
249 
250                 int line = topLine + 1;
251 
252                 while ((y < visibleRect.y + visibleRect.height + ascent) && (line <= rTextArea.getLineCount())) {
253                     paintLineNumber(g, metrics, rhs, y, line);
254 
255                     y += cellHeight;
256 
257                     if (fm != null) {
258                         Fold fold = fm.getFoldForLine(line - 1);
259                         // Skip to next line to paint, taking extra care for lines with
260                         // block ends and begins together, e.g. "} else {"
261                         while ((fold != null) && fold.isCollapsed()) {
262                             int hiddenLineCount = fold.getLineCount();
263                             if (hiddenLineCount == 0) {
264                                 // Fold parser identified a 0-line fold region... This
265                                 // is really a bug, but we'll handle it gracefully.
266                                 break;
267                             }
268                             line += hiddenLineCount;
269                             fold = fm.getFoldForLine(line - 1);
270                         }
271                     }
272 
273                     line++;
274                 }
275             }
276         }
277 
paintLineNumber(Graphics g, FontMetrics metrics, int x, int y, int lineNumber)278         protected void paintLineNumber(Graphics g, FontMetrics metrics, int x, int y, int lineNumber) {
279             int originalLineNumber;
280 
281             if (lineNumberMap != null) {
282                 originalLineNumber = (lineNumber < lineNumberMap.length) ? lineNumberMap[lineNumber] : 0;
283             } else {
284                 originalLineNumber = lineNumber;
285             }
286 
287             if (originalLineNumber != 0) {
288                 String number = Integer.toString(originalLineNumber);
289                 int strWidth = metrics.stringWidth(number);
290                 g.setColor(showMisalignment && (lineNumber != originalLineNumber) ? errorForeground : getForeground());
291                 g.drawString(number, x-strWidth, y);
292             }
293         }
294 
getRhsBorderWidth()295         public int getRhsBorderWidth() { return ((RSyntaxTextArea)rTextArea).isCodeFoldingEnabled() ? 0 : 4; }
296 
297         @Override
getPreferredSize()298         public Dimension getPreferredSize() {
299             if (preferredSize == null) {
300                 int lineCount = getMaximumSourceLineNumber();
301 
302                 if (lineCount > 0) {
303                     Font font = getFont();
304                     FontMetrics fontMetrics = getFontMetrics(font);
305                     int count = 1;
306 
307                     while (lineCount >= 10) {
308                         lineCount = lineCount / 10;
309                         count++;
310                     }
311 
312                     int preferredWidth = fontMetrics.charWidth('9') * count + 10;
313                     preferredSize = new Dimension(preferredWidth, 0);
314                 } else {
315                     preferredSize = new Dimension(0, 0);
316                 }
317             }
318 
319             return preferredSize;
320         }
321     }
322 }
323