1 /*
2  * This file is part of the LibreOffice project.
3  *
4  * This Source Code Form is subject to the terms of the Mozilla Public
5  * License, v. 2.0. If a copy of the MPL was not distributed with this
6  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7  *
8  * This file incorporates work covered by the following license notice:
9  *
10  *   Licensed to the Apache Software Foundation (ASF) under one or more
11  *   contributor license agreements. See the NOTICE file distributed
12  *   with this work for additional information regarding copyright
13  *   ownership. The ASF licenses this file to you under the Apache
14  *   License, Version 2.0 (the "License"); you may not use this file
15  *   except in compliance with the License. You may obtain a copy of
16  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
17  */
18 package com.sun.star.script.framework.provider.beanshell;
19 
20 import java.awt.Color;
21 import java.awt.Dimension;
22 import java.awt.Font;
23 import java.awt.FontMetrics;
24 import java.awt.Graphics;
25 import java.awt.Polygon;
26 import java.awt.Rectangle;
27 import java.awt.event.ActionEvent;
28 import java.awt.event.InputEvent;
29 import java.awt.event.KeyAdapter;
30 import java.awt.event.KeyEvent;
31 
32 import javax.swing.AbstractAction;
33 import javax.swing.JComponent;
34 import javax.swing.JScrollPane;
35 import javax.swing.JTextArea;
36 import javax.swing.KeyStroke;
37 import javax.swing.UIManager;
38 import javax.swing.event.DocumentEvent;
39 import javax.swing.event.DocumentListener;
40 import javax.swing.event.UndoableEditEvent;
41 import javax.swing.event.UndoableEditListener;
42 import javax.swing.text.BadLocationException;
43 import javax.swing.undo.CompoundEdit;
44 import javax.swing.undo.UndoManager;
45 import java.util.List;
46 import java.util.ArrayList;
47 
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50 
51 public class PlainSourceView extends JScrollPane implements
52     ScriptSourceView, DocumentListener {
53 
54     private final ScriptSourceModel model;
55     private JTextArea ta;
56     private GlyphGutter gg;
57     private int linecount;
58     private boolean isModified = false;
59     private static final String undoKey = "Undo";
60     private static final String redoKey = "Redo";
61     private CompoundEdit compoundEdit = null;
62     private static final int noLimit = -1;
63     UndoManager undoManager;
64     private List<UnsavedChangesListener> unsavedListener = new ArrayList<UnsavedChangesListener>();
65 
66     private static final Pattern tabPattern = Pattern.compile("^ *(\\t)");
67     private static final Pattern indentationPattern = Pattern.compile("^([^\\S\\r\\n]*)(([^\\{])*\\{\\s*)*");
68 
PlainSourceView(ScriptSourceModel model)69     public PlainSourceView(ScriptSourceModel model) {
70         this.model = model;
71         initUI();
72         model.setView(this);
73     }
74 
undo()75     public void undo(){
76         if(compoundEdit!=null){
77             compoundEdit.end();
78             undoManager.addEdit(compoundEdit);
79             compoundEdit = null;
80         }
81         if(undoManager.canUndo()){
82             undoManager.undo();
83         }
84         // check if it's the last undoable change
85         if(undoManager.canUndo() == false){
86             setModified(false);
87         }
88     }
redo()89     public void redo(){
90         if(undoManager.canRedo()){
91             undoManager.redo();
92         }
93     }
clear()94     public void clear() {
95         ta.setText("");
96     }
97 
update()98     public void update() {
99         /* Remove ourselves as a DocumentListener while loading the source
100            so we don't get a storm of DocumentEvents during loading */
101         ta.getDocument().removeDocumentListener(this);
102 
103         if (!isModified) {
104             int pos = ta.getCaretPosition();
105             ta.setText(model.getText());
106 
107             try {
108                 ta.setCaretPosition(pos);
109             } catch (IllegalArgumentException iae) {
110                 // do nothing and allow JTextArea to set its own position
111             }
112         }
113 
114         // scroll to currentPosition of the model
115         try {
116             int line = ta.getLineStartOffset(model.getCurrentPosition());
117             Rectangle rect = ta.modelToView(line);
118             if (rect != null) {
119                 ta.scrollRectToVisible(rect);
120             }
121         } catch (BadLocationException e) {
122             // couldn't scroll to line, do nothing
123         }
124 
125         gg.repaint();
126 
127         // Add back the listener
128         ta.getDocument().addDocumentListener(this);
129     }
130 
isModified()131     public boolean isModified() {
132         return isModified;
133     }
134 
notifyListeners(boolean isUnsaved)135     private void notifyListeners (boolean isUnsaved) {
136         for (UnsavedChangesListener listener : unsavedListener) {
137             listener.onUnsavedChanges(isUnsaved);
138         }
139     }
140 
setModified(boolean value)141     public void setModified(boolean value) {
142         if(value != isModified) {
143             notifyListeners(value);
144             isModified = value;
145         }
146     }
147 
initUI()148     private void initUI() {
149         try{
150                 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
151         }
152         catch(Exception e){
153                 // What to do here
154         }
155         ta = new JTextArea();
156         ta.setTabSize(4);
157         ta.setRows(15);
158         ta.setColumns(40);
159         ta.setLineWrap(false);
160         ta.insert(model.getText(), 0);
161         ta.setFont(new Font("Monospaced", ta.getFont().getStyle(), ta.getFont().getSize()));
162         undoManager = new UndoManager();
163         undoManager.setLimit(noLimit);
164         ta.getDocument().addUndoableEditListener(new UndoableEditListener(){
165             @Override
166             public void undoableEditHappened(UndoableEditEvent editEvent) {
167                 if(compoundEdit == null){
168                     compoundEdit = new CompoundEdit();
169                 }
170                 compoundEdit.addEdit(editEvent.getEdit());
171             }
172         });
173 
174         ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_MASK), undoKey);
175         ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_MASK), redoKey);
176 
177         ta.addKeyListener(new KeyAdapter(){
178             @Override
179             public void keyPressed(KeyEvent ke) {
180                 // if shift + tab was pressed, remove the first tab before any code begins
181                 if (ke.isShiftDown() && ke.getKeyCode() == KeyEvent.VK_TAB) {
182                     try {
183                         int caretOffset = ta.getCaretPosition();
184                         int lineOffset = ta.getLineOfOffset(caretOffset);
185                         int startOffset = ta.getLineStartOffset(lineOffset);
186                         int endOffset = ta.getLineEndOffset(lineOffset);
187 
188                         Matcher matcher = tabPattern.matcher(ta.getText(startOffset, endOffset - startOffset));
189                         if (matcher.find()) {
190                             ta.replaceRange(null, startOffset + matcher.start(1), startOffset + matcher.end(1));
191                         }
192                     } catch (BadLocationException e) {
193                         // could not find correct location of the tab
194                     }
195                 }
196                 // if the enter key was pressed, adjust indentation of the current line accordingly
197                 if (ke.getKeyCode() == KeyEvent.VK_ENTER) {
198                     try {
199                         int caretOffset = ta.getCaretPosition();
200                         int lineOffset = ta.getLineOfOffset(caretOffset);
201                         int startOffset = ta.getLineStartOffset(lineOffset);
202                         int endOffset = ta.getLineEndOffset(lineOffset);
203 
204                         Matcher matcher = indentationPattern.matcher(ta.getText(startOffset, endOffset - startOffset));
205                         // insert new line including indentation of the previous line
206                         ta.insert("\n", caretOffset++);
207                         if (matcher.find()) {
208                             if (matcher.group(1).length() > 0) {
209                                 ta.insert(matcher.group(1), caretOffset++);
210                             }
211                             // if there is an open curly bracket in the current line, increase indentation level
212                             if (matcher.group(3) != null) {
213                                 ta.insert("\t", caretOffset);
214                             }
215                         }
216                         ke.consume();
217                     } catch (BadLocationException e) {
218                         // could not find correct location of the indentation
219                     }
220                 }
221             }
222 
223             @Override
224             public void keyReleased(KeyEvent ke){
225                 if(ke.getKeyCode() == KeyEvent.VK_SPACE || ke.getKeyCode() == KeyEvent.VK_ENTER){
226                     compoundEdit.end();
227                     undoManager.addEdit(compoundEdit);
228                     compoundEdit = null;
229                 }
230             }
231         });
232 
233         ta.getActionMap().put(undoKey, new AbstractAction(undoKey){
234             @Override
235             public void actionPerformed(ActionEvent event) {
236                 undo();
237             }
238         });
239 
240         ta.getActionMap().put(redoKey, new AbstractAction(redoKey){
241             @Override
242             public void actionPerformed(ActionEvent event) {
243                 redo();
244             }
245         });
246 
247         linecount = ta.getLineCount();
248 
249         gg = new GlyphGutter(this);
250 
251         setViewportView(ta);
252         setRowHeaderView(gg);
253 
254         ta.getDocument().addDocumentListener(this);
255     }
256 
257     /* Implementation of DocumentListener interface */
insertUpdate(DocumentEvent e)258     public void insertUpdate(DocumentEvent e) {
259         doChanged();
260     }
261 
removeUpdate(DocumentEvent e)262     public void removeUpdate(DocumentEvent e) {
263         doChanged();
264     }
265 
changedUpdate(DocumentEvent e)266     public void changedUpdate(DocumentEvent e) {
267         doChanged();
268     }
269 
270     /* If the number of lines in the JTextArea has changed then update the
271        GlyphGutter */
doChanged()272     private void doChanged() {
273         setModified(true);
274 
275         if (linecount != ta.getLineCount()) {
276             gg.update();
277             linecount = ta.getLineCount();
278         }
279     }
280 
getText()281     public String getText() {
282         return ta.getText();
283     }
284 
getTextArea()285     public JTextArea getTextArea() {
286         return ta;
287     }
288 
getCurrentPosition()289     public int getCurrentPosition() {
290         return model.getCurrentPosition();
291     }
292 
addListener(UnsavedChangesListener toAdd)293     public void addListener(UnsavedChangesListener toAdd) {
294         unsavedListener.add(toAdd);
295     }
296 }
297 
298 class GlyphGutter extends JComponent {
299 
300     private final PlainSourceView view;
301     private static final String DUMMY_STRING = "99";
302 
GlyphGutter(PlainSourceView view)303     GlyphGutter(PlainSourceView view) {
304         this.view = view;
305         update();
306     }
307 
update()308     public void update() {
309         JTextArea textArea = view.getTextArea();
310         Font font = textArea.getFont();
311         setFont(font);
312 
313         FontMetrics metrics = getFontMetrics(font);
314         int h = metrics.getHeight();
315         int lineCount = textArea.getLineCount() + 1;
316 
317         String dummy = Integer.toString(lineCount);
318 
319         if (dummy.length() < 2) {
320             dummy = DUMMY_STRING;
321         }
322 
323         Dimension d = new Dimension();
324         d.width = metrics.stringWidth(dummy) + 16;
325         d.height = lineCount * h + 100;
326         setPreferredSize(d);
327         setSize(d);
328     }
329 
330     @Override
paintComponent(Graphics g)331     public void paintComponent(Graphics g) {
332         JTextArea textArea = view.getTextArea();
333 
334         Font font = textArea.getFont();
335         g.setFont(font);
336 
337         FontMetrics metrics = getFontMetrics(font);
338         Rectangle clip = g.getClipBounds();
339 
340         g.setColor(getBackground());
341         g.fillRect(clip.x, clip.y, clip.width, clip.height);
342 
343         int ascent = metrics.getMaxAscent();
344         int h = metrics.getHeight();
345         int lineCount = textArea.getLineCount() + 1;
346 
347         int startLine = clip.y / h;
348         int endLine = (clip.y + clip.height) / h + 1;
349         int width = getWidth();
350 
351         if (endLine > lineCount) {
352             endLine = lineCount;
353         }
354 
355         for (int i = startLine; i < endLine; i++) {
356             String text;
357             text = Integer.toString(i + 1) + " ";
358             int y = i * h;
359             g.setColor(Color.blue);
360             g.drawString(text, 0, y + ascent);
361             int x = width - ascent;
362 
363             // if currentPosition is not -1 then a red arrow will be drawn
364             if (i == view.getCurrentPosition()) {
365                 drawArrow(g, ascent, x, y);
366             }
367         }
368     }
369 
drawArrow(Graphics g, int ascent, int x, int y)370     private void drawArrow(Graphics g, int ascent, int x, int y) {
371         Polygon arrow = new Polygon();
372         int dx = x;
373         y += ascent - 10;
374         int dy = y;
375         arrow.addPoint(dx, dy + 3);
376         arrow.addPoint(dx + 5, dy + 3);
377 
378         for (x = dx + 5; x <= dx + 10; x++, y++) {
379             arrow.addPoint(x, y);
380         }
381 
382         for (x = dx + 9; x >= dx + 5; x--, y++) {
383             arrow.addPoint(x, y);
384         }
385 
386         arrow.addPoint(dx + 5, dy + 7);
387         arrow.addPoint(dx, dy + 7);
388 
389         g.setColor(Color.red);
390         g.fillPolygon(arrow);
391         g.setColor(Color.black);
392         g.drawPolygon(arrow);
393     }
394 }
395 
396