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.*;
11 import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
12 import org.fife.ui.rtextarea.*;
13 import org.jd.gui.api.feature.ContentSearchable;
14 import org.jd.gui.api.feature.LineNumberNavigable;
15 import org.jd.gui.api.feature.PreferencesChangeListener;
16 import org.jd.gui.api.feature.UriOpenable;
17 import org.jd.gui.util.exception.ExceptionUtil;
18 
19 import javax.swing.*;
20 import javax.swing.text.BadLocationException;
21 import java.awt.*;
22 import java.awt.event.KeyEvent;
23 import java.awt.event.MouseAdapter;
24 import java.awt.event.MouseEvent;
25 import java.awt.event.MouseWheelListener;
26 import java.io.IOException;
27 import java.io.UnsupportedEncodingException;
28 import java.net.URI;
29 import java.net.URLDecoder;
30 import java.util.HashMap;
31 import java.util.Map;
32 
33 public class AbstractTextPage extends JPanel implements LineNumberNavigable, ContentSearchable, UriOpenable, PreferencesChangeListener {
34     protected static final String FONT_SIZE_KEY = "ViewerPreferences.fontSize";
35 
36     protected static final ImageIcon COLLAPSED_ICON = new ImageIcon(AbstractTextPage.class.getClassLoader().getResource("org/jd/gui/images/plus.png"));
37     protected static final ImageIcon EXPANDED_ICON = new ImageIcon(AbstractTextPage.class.getClassLoader().getResource("org/jd/gui/images/minus.png"));
38 
39     protected static final Color DOUBLE_CLICK_HIGHLIGHT_COLOR = new Color(0x66ff66);
40     protected static final Color SEARCH_HIGHLIGHT_COLOR = new Color(0xffff66);
41     protected static final Color SELECT_HIGHLIGHT_COLOR = new Color(0xF49810);
42 
43     protected static final RSyntaxTextAreaEditorKit.DecreaseFontSizeAction DECREASE_FONT_SIZE_ACTION = new RSyntaxTextAreaEditorKit.DecreaseFontSizeAction();
44     protected static final RSyntaxTextAreaEditorKit.IncreaseFontSizeAction INCREASE_FONT_SIZE_ACTION = new RSyntaxTextAreaEditorKit.IncreaseFontSizeAction();
45 
46     protected RSyntaxTextArea textArea;
47     protected RTextScrollPane scrollPane;
48 
49     protected Map<String, String> preferences;
50 
AbstractTextPage()51     public AbstractTextPage() {
52         super(new BorderLayout());
53 
54         textArea = newSyntaxTextArea();
55         textArea.setSyntaxEditingStyle(getSyntaxStyle());
56         textArea.setCodeFoldingEnabled(true);
57         textArea.setAntiAliasingEnabled(true);
58         textArea.setCaretPosition(0);
59         textArea.setEditable(false);
60         textArea.setDropTarget(null);
61         textArea.setPopupMenu(null);
62         textArea.addMouseListener(new MouseAdapter() {
63             public void mouseClicked(MouseEvent e) {
64                 if (e.getClickCount() == 2) {
65                     textArea.setMarkAllHighlightColor(DOUBLE_CLICK_HIGHLIGHT_COLOR);
66                     SearchEngine.markAll(textArea, newSearchContext(textArea.getSelectedText(), true, true, true, false));
67                 }
68             }
69         });
70 
71         KeyStroke ctrlA = KeyStroke.getKeyStroke(KeyEvent.VK_A, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
72         KeyStroke ctrlC = KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
73         KeyStroke ctrlV = KeyStroke.getKeyStroke(KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
74         InputMap inputMap = textArea.getInputMap();
75         inputMap.put(ctrlA, "none");
76         inputMap.put(ctrlC, "none");
77         inputMap.put(ctrlV, "none");
78 
79         try {
80             Theme theme = Theme.load(getClass().getClassLoader().getResourceAsStream("rsyntaxtextarea/themes/eclipse.xml"));
81             theme.apply(textArea);
82         } catch (IOException e) {
83             assert ExceptionUtil.printStackTrace(e);
84         }
85 
86         scrollPane = new RTextScrollPane(textArea);
87         scrollPane.setFoldIndicatorEnabled(true);
88         scrollPane.setFont(textArea.getFont());
89 
90         final MouseWheelListener[] mouseWheelListeners = scrollPane.getMouseWheelListeners();
91 
92         // Remove default listeners
93         for (MouseWheelListener listener : mouseWheelListeners) {
94             scrollPane.removeMouseWheelListener(listener);
95         }
96 
97         scrollPane.addMouseWheelListener(e -> {
98             if ((e.getModifiers() & (Event.META_MASK|Event.CTRL_MASK)) != 0) {
99                 int x = e.getX() + scrollPane.getX() - textArea.getX();
100                 int y = e.getY() + scrollPane.getY() - textArea.getY();
101                 int offset = textArea.viewToModel(new Point(x, y));
102 
103                 // Update font size
104                 if (e.getWheelRotation() > 0) {
105                     DECREASE_FONT_SIZE_ACTION.actionPerformedImpl(null, textArea);
106                 } else {
107                     INCREASE_FONT_SIZE_ACTION.actionPerformedImpl(null, textArea);
108                 }
109 
110                 // Save preferences
111                 if (preferences != null) {
112                     preferences.put(FONT_SIZE_KEY, String.valueOf(textArea.getFont().getSize()));
113                 }
114 
115                 try {
116                     Rectangle newRectangle = textArea.modelToView(offset);
117                     int newY = newRectangle.y + (newRectangle.height >> 1);
118 
119                     // Scroll
120                     Point viewPosition = scrollPane.getViewport().getViewPosition();
121                     viewPosition.y = Math.max(viewPosition.y +newY - y, 0);
122                     scrollPane.getViewport().setViewPosition(viewPosition);
123                 } catch (BadLocationException ee) {
124                     assert ExceptionUtil.printStackTrace(ee);
125                 }
126             } else {
127                 // Call default listeners
128                 for (MouseWheelListener listener : mouseWheelListeners) {
129                     listener.mouseWheelMoved(e);
130                 }
131             }
132         });
133 
134         Gutter gutter = scrollPane.getGutter();
135         gutter.setFoldIcons(COLLAPSED_ICON, EXPANDED_ICON);
136         gutter.setFoldIndicatorForeground(gutter.getBorderColor());
137 
138         add(scrollPane, BorderLayout.CENTER);
139         add(new RoundMarkErrorStrip(textArea), BorderLayout.LINE_END);
140     }
141 
newSyntaxTextArea()142     protected RSyntaxTextArea newSyntaxTextArea() { return new RSyntaxTextArea(); }
143 
getText()144     public String getText() { return textArea.getText(); }
145 
getScrollPane()146     public JScrollPane getScrollPane() {
147         return scrollPane;
148     }
149 
setText(String text)150     public void setText(String text) {
151         textArea.setText(text);
152         textArea.setCaretPosition(0);
153     }
154 
getSyntaxStyle()155     public String getSyntaxStyle() { return SyntaxConstants.SYNTAX_STYLE_NONE; }
156 
157     /**
158      * @see org.fife.ui.rsyntaxtextarea.RSyntaxUtilities#selectAndPossiblyCenter
159      * Force center and do not select
160      */
setCaretPositionAndCenter(DocumentRange range)161     public void setCaretPositionAndCenter(DocumentRange range) {
162         final int start = range.getStartOffset();
163         final int end = range.getEndOffset();
164         boolean foldsExpanded = false;
165         FoldManager fm = textArea.getFoldManager();
166 
167         if (fm.isCodeFoldingSupportedAndEnabled()) {
168             foldsExpanded = fm.ensureOffsetNotInClosedFold(start);
169             foldsExpanded |= fm.ensureOffsetNotInClosedFold(end);
170         }
171 
172         if (!foldsExpanded) {
173             try {
174                 Rectangle rec = textArea.modelToView(start);
175 
176                 if (rec != null) {
177                     // Visible
178                     setCaretPositionAndCenter(start, end, rec);
179                 } else {
180                     // Not visible yet
181                     SwingUtilities.invokeLater(() -> {
182                         try {
183                             Rectangle r = textArea.modelToView(start);
184                             if (r != null) {
185                                 setCaretPositionAndCenter(start, end, r);
186                             }
187                         } catch (BadLocationException e) {
188                             assert ExceptionUtil.printStackTrace(e);
189                         }
190                     });
191                 }
192             } catch (BadLocationException e) {
193                 assert ExceptionUtil.printStackTrace(e);
194             }
195         }
196     }
197 
setCaretPositionAndCenter(int start, int end, Rectangle r)198     protected void setCaretPositionAndCenter(int start, int end, Rectangle r) {
199         if (end != start) {
200             try {
201                 r = r.union(textArea.modelToView(end));
202             } catch (BadLocationException e) {
203                 assert ExceptionUtil.printStackTrace(e);
204             }
205         }
206 
207         Rectangle visible = textArea.getVisibleRect();
208 
209         // visible.x = r.x - (visible.width - r.width) / 2;
210         visible.y = r.y - (visible.height - r.height) / 2;
211 
212         Rectangle bounds = textArea.getBounds();
213         Insets i = textArea.getInsets();
214         //bounds.x = i.left;
215         bounds.y = i.top;
216         //bounds.width -= i.left + i.right;
217         bounds.height -= i.top + i.bottom;
218 
219         //if (visible.x < bounds.x) {
220         //    visible.x = bounds.x;
221         //}
222         //if (visible.x + visible.width > bounds.x + bounds.width) {
223         //    visible.x = bounds.x + bounds.width - visible.width;
224         //}
225         if (visible.y < bounds.y) {
226             visible.y = bounds.y;
227         }
228         if (visible.y + visible.height > bounds.y + bounds.height) {
229             visible.y = bounds.y + bounds.height - visible.height;
230         }
231 
232         textArea.scrollRectToVisible(visible);
233         textArea.setCaretPosition(start);
234     }
235 
236     // --- LineNumberNavigable --- //
getMaximumLineNumber()237     public int getMaximumLineNumber() {
238         try {
239             return textArea.getLineOfOffset(textArea.getDocument().getLength()) + 1;
240         } catch (BadLocationException e) {
241             assert ExceptionUtil.printStackTrace(e);
242             return 0;
243         }
244     }
245 
goToLineNumber(int lineNumber)246     public void goToLineNumber(int lineNumber) {
247         try {
248             textArea.setCaretPosition(textArea.getLineStartOffset(lineNumber-1));
249         } catch (BadLocationException e) {
250             assert ExceptionUtil.printStackTrace(e);
251         }
252     }
253 
checkLineNumber(int lineNumber)254     public boolean checkLineNumber(int lineNumber) { return true; }
255 
256     // --- ContentSearchable --- //
highlightText(String text, boolean caseSensitive)257     public boolean highlightText(String text, boolean caseSensitive) {
258         if (text.length() > 1) {
259             textArea.setMarkAllHighlightColor(SEARCH_HIGHLIGHT_COLOR);
260             textArea.setCaretPosition(textArea.getSelectionStart());
261 
262             SearchContext context = newSearchContext(text, caseSensitive, false, true, false);
263             SearchResult result = SearchEngine.find(textArea, context);
264 
265             if (!result.wasFound()) {
266                 textArea.setCaretPosition(0);
267                 result = SearchEngine.find(textArea, context);
268             }
269 
270             return result.wasFound();
271         } else {
272             return true;
273         }
274     }
275 
findNext(String text, boolean caseSensitive)276     public void findNext(String text, boolean caseSensitive) {
277         if (text.length() > 1) {
278             textArea.setMarkAllHighlightColor(SEARCH_HIGHLIGHT_COLOR);
279 
280             SearchContext context = newSearchContext(text, caseSensitive, false, true, false);
281             SearchResult result = SearchEngine.find(textArea, context);
282 
283             if (!result.wasFound()) {
284                 textArea.setCaretPosition(0);
285                 SearchEngine.find(textArea, context);
286             }
287         }
288     }
289 
findPrevious(String text, boolean caseSensitive)290     public void findPrevious(String text, boolean caseSensitive) {
291         if (text.length() > 1) {
292             textArea.setMarkAllHighlightColor(SEARCH_HIGHLIGHT_COLOR);
293 
294             SearchContext context = newSearchContext(text, caseSensitive, false, false, false);
295             SearchResult result = SearchEngine.find(textArea, context);
296 
297             if (!result.wasFound()) {
298                 textArea.setCaretPosition(textArea.getDocument().getLength());
299                 SearchEngine.find(textArea, context);
300             }
301         }
302     }
303 
newSearchContext(String searchFor, boolean matchCase, boolean wholeWord, boolean searchForward, boolean regexp)304     protected SearchContext newSearchContext(String searchFor, boolean matchCase, boolean wholeWord, boolean searchForward, boolean regexp) {
305         SearchContext context = new SearchContext(searchFor, matchCase);
306         context.setMarkAll(true);
307         context.setWholeWord(wholeWord);
308         context.setSearchForward(searchForward);
309         context.setRegularExpression(regexp);
310         return context;
311     }
312 
313     // --- UriOpenable --- //
openUri(URI uri)314     public boolean openUri(URI uri) {
315         String query = uri.getQuery();
316 
317         if (query != null) {
318             Map<String, String> parameters = parseQuery(query);
319 
320             if (parameters.containsKey("lineNumber")) {
321                 String lineNumber = parameters.get("lineNumber");
322 
323                 try {
324                     goToLineNumber(Integer.parseInt(lineNumber));
325                     return true;
326                 } catch (NumberFormatException e) {
327                     assert ExceptionUtil.printStackTrace(e);
328                 }
329             } else if (parameters.containsKey("position")) {
330                 String position = parameters.get("position");
331 
332                 try {
333                     int pos = Integer.parseInt(position);
334                     if (textArea.getDocument().getLength() > pos) {
335                         setCaretPositionAndCenter(new DocumentRange(pos, pos));
336                         return true;
337                     }
338                 } catch (NumberFormatException e) {
339                     assert ExceptionUtil.printStackTrace(e);
340                 }
341             } else if (parameters.containsKey("highlightFlags")) {
342                 String highlightFlags = parameters.get("highlightFlags");
343 
344                 if ((highlightFlags.indexOf('s') != -1) && parameters.containsKey("highlightPattern")) {
345                     textArea.setMarkAllHighlightColor(SELECT_HIGHLIGHT_COLOR);
346                     textArea.setCaretPosition(0);
347 
348                     // Highlight all
349                     String searchFor = createRegExp(parameters.get("highlightPattern"));
350                     SearchContext context =  newSearchContext(searchFor, true, false, true, true);
351                     SearchResult result = SearchEngine.find(textArea, context);
352 
353                     if (result.getMatchRange() != null) {
354                         textArea.setCaretPosition(result.getMatchRange().getStartOffset());
355                     }
356 
357                     return true;
358                 }
359             }
360         }
361 
362         return false;
363     }
364 
parseQuery(String query)365     protected Map<String, String> parseQuery(String query) {
366         HashMap<String, String> parameters = new HashMap<>();
367 
368         // Parse parameters
369         try {
370             for (String param : query.split("&")) {
371                 int index = param.indexOf('=');
372 
373                 if (index == -1) {
374                     parameters.put(URLDecoder.decode(param, "UTF-8"), "");
375                 } else {
376                     String key = param.substring(0, index);
377                     String value = param.substring(index + 1);
378                     parameters.put(URLDecoder.decode(key, "UTF-8"), URLDecoder.decode(value, "UTF-8"));
379                 }
380             }
381         } catch (UnsupportedEncodingException e) {
382             assert ExceptionUtil.printStackTrace(e);
383         }
384 
385         return parameters;
386     }
387 
388     /**
389      * Create a simple regular expression
390      *
391      * Rules:
392      *  '*'        matchTypeEntries 0 ou N characters
393      *  '?'        matchTypeEntries 1 character
394      */
createRegExp(String pattern)395     public static String createRegExp(String pattern) {
396         int patternLength = pattern.length();
397         StringBuilder sbPattern = new StringBuilder(patternLength * 2);
398 
399         for (int i = 0; i < patternLength; i++) {
400             char c = pattern.charAt(i);
401 
402             if (c == '*') {
403                 sbPattern.append(".*");
404             } else if (c == '?') {
405                 sbPattern.append('.');
406             } else if (c == '.') {
407                 sbPattern.append("\\.");
408             } else {
409                 sbPattern.append(c);
410             }
411         }
412 
413         return sbPattern.toString();
414     }
415 
416     // --- PreferencesChangeListener --- //
preferencesChanged(Map<String, String> preferences)417     public void preferencesChanged(Map<String, String> preferences) {
418         String fontSize = preferences.get(FONT_SIZE_KEY);
419 
420         if (fontSize != null) {
421             try {
422                 textArea.setFont(textArea.getFont().deriveFont(Float.parseFloat(fontSize)));
423             } catch (Exception e) {
424                 assert ExceptionUtil.printStackTrace(e);
425             }
426         }
427 
428         this.preferences = preferences;
429     }
430 }
431